Telerik blogs

Learn how to use the .NET MAUI Community Toolkit CameraView control in your app, from initializing to creating the user interface.

A functionality that is common in mobile applications is the ability to work with the camera. Although the .NET MAUI framework includes classes and methods to use the camera, having a component that allows creating interfaces that offer additional functionality helps speed up application development. That is why in this article, we will examine the CameraView control of the .NET MAUI Community Toolkit.

What Is the CameraView Control

CameraView is a control within the CommunityToolkit.Maui.Camera package that helps perform tasks beyond taking photos and videos, allowing control of features such as flash, zoom, saving files and even hooks for reacting to different events.

This allows you to create experiences within your application without needing to navigate to an external application, giving you full control of the buttons, visual state and destination of captured files.

Using the CameraView Control in Your .NET MAUI Applications

Let’s demonstrate the CameraView control’s potential. To do this, in your project open the NuGet package manager and install the following packages:

CommunityToolkit.Maui.Camera
CommunityToolkit.Mvvm

The first package is the one we need to use the control and the APIs, while the second will allow using the MVVM pattern more easily in the project.

Once you have installed the camera control package, you will see a ReadMe.txt file with the steps to follow to correctly configure the control. This file tells us that we must go to MauiProgram.cs, adding the UseMauiCommunityToolkitCamera method for the purpose of initializing the control:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkitCamera()           
		...
    }
}

With the control registered, we are ready to try it.

Declaring Permissions Per Platform

Although the readme doesn’t specify it, it is well known that to use hardware features on physical devices you must request permission from the user to use them. In our case, it is necessary to request permissions to use the camera and the microphone, which must be configured differently on each platform.

For Android, you should open the file Platforms/Android/AndroidManifest.xml, adding:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" />	
	...	
    <uses-permission android:name="android.permission.CAMERA" />

    <!--Optional. Only for video recording-->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

</manifest>

For iOS, you should open the file Platforms/iOS/Info.plist, adding the following privacy descriptions:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    ...

    <key>NSCameraUsageDescription</key>
    <string>PROVIDE YOUR REASON HERE</string>

    <key>NSMicrophoneUsageDescription</key>
    <string>PROVIDE YOUR REASON HERE</string>
</dict>
</plist>

For Windows no additional action is required.

Creating a ViewModel to Manage the Control

Once we’ve configured the project to request permissions from the user, let’s create the view model, which will allow managing the state of the control. For this, I created a class called MainViewModel with the following content.

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    string statusMessage = "Grant permissions to start the camera.";

    [ObservableProperty]
    string cameraName = "No camera selected";

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasPhoto))]
    string? lastPhotoPath;

    [ObservableProperty]
    string? lastVideoPath;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(RecordButtonText))]
    [NotifyPropertyChangedFor(nameof(RecordButtonColor))]
    bool isRecording;

    [ObservableProperty]
    bool isTorchOn;

    [ObservableProperty]
    float currentZoom = 1.0f;

    [ObservableProperty]
    float minimumZoom = 1.0f;

    [ObservableProperty]
    float maximumZoom = 4.0f;

    [ObservableProperty]
    CameraFlashMode selectedFlashMode = CameraFlashMode.Off;

    [ObservableProperty]
    CameraInfo? selectedCamera;

    public IReadOnlyList<CameraFlashMode> FlashModes { get; } = Enum.GetValues<CameraFlashMode>();

    public bool HasPhoto => !string.IsNullOrWhiteSpace(LastPhotoPath);

    public string RecordButtonText => IsRecording ? "Stop video" : "Record video";

    public Color RecordButtonColor => IsRecording ? Colors.Firebrick : Colors.DarkGreen;

    public void UpdateCameraInfo()
    {
        if (SelectedCamera is null)
        {
            CameraName = "No camera selected";
            return;
        }

        CameraName = $"{SelectedCamera.Name} ({SelectedCamera.Position})";
        MinimumZoom = Math.Max(1.0f, SelectedCamera.MinimumZoomFactor);
        MaximumZoom = Math.Max(MinimumZoom, SelectedCamera.MaximumZoomFactor);
        CurrentZoom = MinimumZoom;
    }

    partial void OnSelectedCameraChanged(CameraInfo? value)
    {
        UpdateCameraInfo();
    }
}

In the previous code, I created some properties that can help us manage controls in the UI, for example:

  • CameraName: To indicate which camera is selected
  • LastPhotoPath and LastVideoPath: To show the latest paths for photos and videos
  • IsRecording: To maintain the recording state
  • CurrentZoom, MinimumZoom and MaximumZoom: To control zoom values
  • OnSelectedCameraChanged(...) and UpdateCameraInfo(): To update values when there is a camera change

With the base viewmodel ready, it’s time to create the graphical interface.

Creating the UI for the Application

It’s time to create the graphical interface page for the application. We’ll do this by creating a ContentPage, replacing the content with the following:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">

    <Grid RowDefinitions="*,Auto" Padding="16" RowSpacing="12">
        <Border Grid.Row="0" StrokeThickness="0" BackgroundColor="Black">
            <Grid>
                <toolkit:CameraView
                    x:Name="Camera"
                    CameraFlashMode="{Binding SelectedFlashMode}"
                    IsTorchOn="{Binding IsTorchOn}"
                    SelectedCamera="{Binding SelectedCamera}"
                    ZoomFactor="{Binding CurrentZoom}" />

                <Border
                    Margin="12"
                    Padding="10,6"
                    BackgroundColor="#99000000"
                    StrokeThickness="0"
                    HorizontalOptions="Start"
                    VerticalOptions="Start">
                    <Label Text="{Binding CameraName}" TextColor="White" FontSize="12" />
                </Border>
            </Grid>
        </Border>

        <ScrollView Grid.Row="1" MaximumHeightRequest="360">
            <VerticalStackLayout Spacing="14">
                <Label Text="CameraView media demo" FontSize="22" FontAttributes="Bold" />
                <Label Text="{Binding StatusMessage}" />

                <Grid ColumnDefinitions="*,*" ColumnSpacing="12">
                    <Button Text="Take photo" Clicked="OnTakePhotoClicked" />
                    <Button
                        Grid.Column="1"
                        Text="{Binding RecordButtonText}"
                        Clicked="OnRecordVideoClicked"
                        BackgroundColor="{Binding RecordButtonColor}" />
                </Grid>

                <Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" RowDefinitions="Auto,Auto,Auto">
                    <Label Text="Zoom" VerticalOptions="Center" />
                    <Slider
                        Grid.Column="1"
                        Minimum="{Binding MinimumZoom}"
                        Maximum="{Binding MaximumZoom}"
                        Value="{Binding CurrentZoom}" />

                    <Label Grid.Row="1" Text="Torch" VerticalOptions="Center" />
                    <Switch Grid.Row="1" Grid.Column="1" IsToggled="{Binding IsTorchOn}" HorizontalOptions="Start" />

                    <Label Grid.Row="2" Text="Flash" VerticalOptions="Center" />
                    <Picker
                        Grid.Row="2"
                        Grid.Column="1"
                        ItemsSource="{Binding FlashModes}"
                        SelectedItem="{Binding SelectedFlashMode}" />
                </Grid>

                <Image
                    Source="{Binding LastPhotoPath}"                    
                    Aspect="AspectFill"
                    IsVisible="{Binding HasPhoto}" />

                <Label Text="{Binding LastPhotoPath}" FontSize="12" LineBreakMode="TailTruncation" />
                <Label Text="{Binding LastVideoPath}" FontSize="12" LineBreakMode="TailTruncation" />
            </VerticalStackLayout>
        </ScrollView>
    </Grid>

</ContentPage>

In the previous code, we can highlight a few things:

  • The namespace xmlns:toolkit is used so that the control can be used via toolkit:CameraView.
  • Properties such as CameraFlashMode, IsTorchOn, SelectedCamera and ZoomFactor are used, which will be connected to the viewmodel’s properties.
  • We use events linked to the code behind OnTakePhotoClicked and OnRecordVideoClicked, because we work with capture APIs and streams.

Connecting the Content Page with the ViewModel

Once we have created the XAML code for the graphical interface, it’s time to connect it with the view model, as well as create the missing event handlers. To do this, replace the code behind of the XAML page with the following content:

public partial class MainPage : ContentPage
{
    readonly MainViewModel viewModel = new();
    Stream recordingStream = Stream.Null;

    public MainPage()
    {
        InitializeComponent();
        BindingContext = viewModel;

        Camera.MediaCaptureFailed += OnMediaCaptureFailed;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();
        await RequestCameraPermissions();
        await LoadCameras();
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        Camera.MediaCaptureFailed -= OnMediaCaptureFailed;
        recordingStream.Dispose();
    }

    private async Task RequestCameraPermissions()
    {
        var cameraStatus = await Permissions.RequestAsync<Permissions.Camera>();
        if (cameraStatus is not PermissionStatus.Granted)
        {
            viewModel.StatusMessage = "You need to grant camera permission.";
            return;
        }

        try
        {
            var microphoneStatus = await Permissions.RequestAsync<Permissions.Microphone>();
            if (microphoneStatus is not PermissionStatus.Granted)
            {
                viewModel.StatusMessage = "You need to grant microphone permission to record video.";
            }
        }
        catch (FileNotFoundException) when (OperatingSystem.IsWindows())
        {
            viewModel.StatusMessage = "On unpackaged Windows apps, microphone permission may require a packaged app.";
        }
    }

    private async Task LoadCameras()
    {
        try
        {
            var cameras = await Camera.GetAvailableCameras(CancellationToken.None);
            viewModel.SelectedCamera = cameras.FirstOrDefault();
            viewModel.UpdateCameraInfo();
        }
        catch (Exception ex)
        {
            viewModel.StatusMessage = $"No cameras were found: {ex.Message}";
        }
    }

    private void OnMediaCaptureFailed(object? sender, MediaCaptureFailedEventArgs e)
    {
        viewModel.StatusMessage = $"Capture error: {e.FailureReason}";
    }
}

In the previous code you can notice some interesting things:

  1. As indicated by the CameraView documentation, permissions must be requested manually from the user to use the camera and microphone, in addition to the per-platform configuration. We do this when the page starts through the method RequestCameraPermissions().
  2. It is possible to obtain the list of available cameras through Camera.GetAvailableCameras.
  3. In case an error occurs, we use the event OnMediaCaptureFailed to handle it.

This is just the beginning. Now, let’s look at how to take a photo and a video.

Taking a Photo with the CameraView Control

In the code-behind, we need a method that allows taking a photograph when pressing a button. To achieve this, we will do it through the following code:

private async void OnTakePhotoClicked(object? sender, EventArgs e)
{
    try
    {
        await using var photoStream = await Camera.CaptureImage(CancellationToken.None);
        var photoPath = Path.Combine(FileSystem.AppDataDirectory, $"photo-{DateTime.Now:yyyyMMdd-HHmmss}.jpg");

        await using var fileStream = File.Create(photoPath);
        await photoStream.CopyToAsync(fileStream);

        viewModel.LastPhotoPath = photoPath;
        viewModel.StatusMessage = "Photo saved successfully.";
    }
    catch (Exception ex)
    {
        viewModel.StatusMessage = $"Unable to take photo: {ex.Message}";
    }
}

In the previous method, we use Camera.CaptureImage to take a photograph, which returns a stream that we store locally. Next, we create a path using a safe location through FileSystemAppDataDirectory, which we use to save the stream with File.Create and CopyToAsync. Finally, we update the preview image and display the created path thanks to LastPhotoPath and StatusMessage.

Recording a Video with CameraView

To implement the recording process, we are going to create three methods. One controls the logic to know whether it is recording, starting or stopping the recording. The other two will allow starting and stopping the recording. The code looks as follows:

private async void OnRecordVideoClicked(object? sender, EventArgs e)
{
    if (viewModel.IsRecording)
    {
        await StopRecording();
        return;
    }

    await StartRecording();
}

private async Task StartRecording()
{
    try
    {
        var videoPath = Path.Combine(FileSystem.AppDataDirectory, $"video-{DateTime.Now:yyyyMMdd-HHmmss}.mp4");
        recordingStream = File.Create(videoPath);

        await Camera.StartVideoRecording(recordingStream, CancellationToken.None);

        viewModel.LastVideoPath = videoPath;
        viewModel.IsRecording = true;
        viewModel.StatusMessage = "Recording video...";
    }
    catch (Exception ex)
    {
        await recordingStream.DisposeAsync();
        recordingStream = Stream.Null;
        viewModel.StatusMessage = $"Unable to start video recording: {ex.Message}";
    }
}

private async Task StopRecording()
{
    try
    {
        await Camera.StopVideoRecording(CancellationToken.None);
        await recordingStream.DisposeAsync();

        recordingStream = Stream.Null;
        viewModel.IsRecording = false;
        viewModel.StatusMessage = "Video saved successfully.";
    }
    catch (Exception ex)
    {
        viewModel.StatusMessage = $"Unable to stop video recording: {ex.Message}";
    }
}

In the previous code, the methods do the following:

  • OnRecordVideoClicked(...): Through the property IsRecording, it determines which method should be invoked when the user presses the button in the UI.
  • StartVideoRecording(): It obtains a path using the current time to name the file and creates a stream at that location. Then, it uses the method Camera.StartVideoRecording to start the recording. It is important to keep the stream open while the recording is active.
  • StopRecording: This ends the capture using Camera.StopVideoRecording. After stopping the recording, the stream is closed to release the file.

Testing the Application

After adding all the previous code to the application, it’s time to test the functionality. When running the application, we get the following result:

CameraView showing a live camera preview on screen

You can see that all features work correctly, and that it’s possible to take photos and videos within the same application.

Conclusion

In this article you have learned about the CameraView control from the Community Toolkit, and how to integrate it into your own .NET MAUI applications. We went step by step to initialize it, configure permissions, create the view model and the user interface. With what you’ve learned, you can now create your own experiences using the control in more complex scenarios.


About the Author

Héctor Pérez

Héctor Pérez is a Microsoft MVP with more than 10 years of experience in software development. He is an independent consultant, working with business and government clients to achieve their goals. Additionally, he is an author of books and an instructor at El Camino Dev and Devs School.

 

Related Posts

Comments

Comments are disabled in preview mode.