Avalonia UI – Native Video Playback featuring LibVLCSharp and ExoPlayer

Published:

Modified:

Introduction

We’ll create an Avalonia UI C# project and play URI based audio/video media content. We’ll use LibVLCSharp on Windows, and ExoPlayer on Android.

This demo is a follow-up on my previous post (Using Avalonia UI framework and LibVLCSharp library to play URI based audio), in which I used LibVLCSharp to play audio on Windows and Android.

Initially, my idea was to use LibVLCSharp to play both audio and video, on both Windows and Android, but it turned out this is not possible, because LibVLCSharp VideoView control doesn’t work on Android. I needed a different solution and this is where Avalonia UI support for native views/controls jumped in.

Source Code

The complete source code is available in my GitHub repository.

The Goal

This demo is about:

This demo is NOT about:

Step by Step Walk-through

I imagined this to be a beginner tutorial, so I’ll lead you through it step by step.

Step 1 – Create the project

Launch Visual Studio 2022 and hit the “Create a new project” button:

Avalonia UI - Native Video Playback - 01 - Create a new project
Avalonia UI – Native Video Playback – Create a new project

Step 2 – Select project type

Let’s select “Avalonia C# project” and hit Next:

Avalonia UI - Native Video Playback - 02 - Select project type
Avalonia UI – Native Video Playback – Select project type

Step 3 – Configure the project (project name and location)

Enter the name you like, enter location or browse to the location and hit Create:

Avalonia UI - Native Video Playback - 03 - Enter project name and location
Avalonia UI – Native Video Playback – Enter project name and location

Step 4 – Avalonia project configuration 1/3 – Target platforms

Now it’s time to start configuring Avalonia UI related stuff. First we need to select target platforms. For this demo I used only Windows Desktop and Android. The nice thing is that we can use the same concept to support other platforms, such as iOS. Hit the Next button after you’re done selecting target platforms:

Avalonia UI - Native Video Playback - 04 - Select target platforms
Avalonia UI – Native Video Playback – Select target platforms

Step 5 – Avalonia project configuration 2/3 – Design pattern

As I stated earlier, this demo is not about MVVM design. I chose Community Toolkit because I’m used to it. We will not use view model so feel free to chose a pattern per your liking and hit Next:

Avalonia UI - Native Video Playback - 05 - Select a design pattern
Avalonia UI – Native Video Playback – Select a design pattern

Step 6 – Avalonia project configuration 3/3 – Features

We will not be using any of these (Compiled Bindings, Embedded Support, Meadow Support). I definitely recommend using compiled bindings for non-trivial projects. For now, just hit the Create button and that will finish the project creation phase:

Avalonia UI - Native Video Playback - 06 - Add features
Avalonia UI – Native Video Playback – Add features

At this point, you should see 3 projects within the solution:

Avalonia UI - Native Video Playback - 07 - Project overview (common, Android and Windows)
Avalonia UI – Native Video Playback – Project overview (common, Android and Windows)

Step 7 – Modify C# nullable context

For all 3 projects, let’s set the Nullable setting to “Disable”. This is just something I like to do. We need to access project properties by right-clicking the project and selecting Properties. Then we need to locate the setting either by scrolling down or by using the search feature.

Avalonia UI - Native Video Playback - 08 - Modify the Nullable option
Avalonia UI – Native Video Playback – Modify the Nullable option

Step 8 – Modify the default Avalonia UI greeting

This is the most important part of the demo 🙂 Let’s locate and open MainViewModel.cs under the ViewModels folder, and modify the Greeting value to "Monsalma - Welcome to Avalonia Multi-Platform Native Video Playback Demo featuring LibVLCSharp and ExoPlayer":

Avalonia UI - Native Video Playback - 09 - Adjust the Greeting field
Avalonia UI – Native Video Playback – Adjust the Greeting field

Step 9 – Run the app

At this point we still haven’t added any audio/video related source code, but let’s try to build the solution and execute both the Windows Desktop project and Android project. When building, you should get 0 errors. The main application window should look like this:

Windows Desktop:

Avalonia UI - Native Video Playback - 10 - Initial execution (Windows)
Avalonia UI – Native Video Playback – Initial execution (Windows)

Android Emulator (Pixel 5 – API 34):

Avalonia UI - Native Video Playback - 11 - Initial execution (Android Emulator)
Avalonia UI – Native Video Playback – Initial execution (Android Emulator)

Step 10 – Install LibVLCSharp package

Now that we’ve verified that we can build and run our application, it’s time to install some NuGet packages. We should first install LibVLCSharp, but only in the Windows Desktop project. We don’t need the package in the common project. The idea is that the common project remains agnostic to the actual video control used in platform specific projects. After installing the package, NuGet Package Manager should show this:

Avalonia UI - Native Video Playback - 12 - Adding LibVLCSharp NuGet package
Avalonia UI – Native Video Playback – Adding LibVLCSharp NuGet package

Step 11 – Install LibVLCSharp.Avalonia package

Next, we need to add LibVLCSharp.Avalonia package, and only to the Windows Desktop project:

Avalonia UI - Native Video Playback - 13 - Adding LibVLCSharp.Avalonia NuGet package
Avalonia UI – Native Video Playback – Adding LibVLCSharp.Avalonia NuGet package

Step 12 – Install VideoLAN.LibVLC.Windows package

Similarly, we need to add VideoLAN.LibVLC.Windows package to the Windows Desktop project:

Avalonia UI - Native Video Playback - 14 - Adding VideoLAN.LibVLC.Windows NuGet package
Avalonia UI – Native Video Playback – Adding VideoLAN.LibVLC.Windows NuGet package

Step 13 – Install Xamarin.AndroidX.Media3.ExoPlayer package

Xamarin.AndroidX.Media3.ExoPlayer contains the player itself. We need to add it only to the Android project:

Avalonia UI - Native Video Playback - 15 - Adding Xamarin.AndroidX.Media3.ExoPlayer NuGet package
Avalonia UI – Native Video Playback – Adding Xamarin.AndroidX.Media3.ExoPlayer NuGet package

Step 14 – Install Xamarin.AndroidX.Media3.Ui

Xamarin.AndroidX.Media3.Ui contains UI (user interface) controls. We need it only in the Android project:

Avalonia UI - Native Video Playback - 16 - Adding Xamarin.AndroidX.Media3.Ui NuGet package
Avalonia UI – Native Video Playback – Adding Xamarin.AndroidX.Media3.Ui NuGet package

Step 15 – The source code – Native media player service

We’ll create a simple service and use a static member within the App class to hold the reference to the service. This is the simplest way of using a service, but it will serve us well in this demo.

INativeMediaPlayerService.cs

public interface INativeMediaPlayerService
{
	Control CreateControl();
	void Play(string uri);
}

CreateControl method returns the media player UI control. The idea is to first create the UI control and then to add it to the visual tree.

The Windows Desktop project will return LibVLC VideoView control. VideoView is a part of LibVLCSharp.Avalonia NuGet package. VideoView class already inherits from Avalonia’s NativeControlHost class so we just need to instantiate it.

The Android project will use a different approach. We’ll need to create a class which inherits NativeControlHost class. I’ll explain this later in the article.

The Play method will simply initiate the playback.

App.axaml.cs

Here we only define the service.

public static INativeMediaPlayerService AppNativeVideoPlayerService { get; set; }

Service initialization – Windows Desktop

In Program.cs, within BuildAvaloniaApp, we simply instantiate VLCPlayerService (to be discussed later):

App.AppNativeVideoPlayerService = new VLCPlayerService();

Service initialization – Android

Similarly, we add Exo player service related code to MainActivity.cs, within the CustomizeAppBuilder method:

App.AppNativeVideoPlayerService = new ExoPlayerService();

Step 16 – The source code – VLC player service (Windows Desktop)

VLCPlayerService.cs

public class VLCPlayerService : INativeMediaPlayerService
{
    private LibVLC MainLibVLC { get; set; }
    private MediaPlayer MainMediaPlayer { get; set; }

    public Control CreateControl()
    {
        // Create player
        MainLibVLC = new LibVLC(enableDebugLogs: true);

        // Create player view
        MainMediaPlayer = new(MainLibVLC);

        // Create player control
        VideoView videoView = new()
        {
            MediaPlayer = MainMediaPlayer
        };

        return videoView;
    }

    public void Play(string uri)
    {
        // Create media
        var media = new Media(MainLibVLC, new Uri(uri));

        // Play media
        MainMediaPlayer.Media = media;
        MainMediaPlayer.Play();
    }
}

As I said earlier, here most of the work has already been done for us. We have a UI control (VideoView) we can use.

CreateControl method instantiates LibVLC engine (LibVLC) and VLC media player (MediaPlayer). Then it creates a VideoView control and sets its media player property.

Play method creates a Media object based on the provided URI, assigns the media to the media player and initiates the playback.

Please note the simplicity of this – we only needed to create a VideoView object and assign a media player to it. LibVLCSharp took care of all the rest.

Step 17 – The source code – Exo player service (Android)

For the Android portion of the demo, we’ll need to dig a bit deeper. We’ll implement a service, which will be pretty similar to its Windows counterpart, but we’ll need to implement a user interface control as well.

ExoPlayerControl.cs

ExoPlayerControl inherits from NativeControlHost. In general, we use NativeControlHost class when we want to gain access to native UI controls. Our original problem was that we could not use one library/control to play video on both Windows and Android. NativeControlHost allows us to overcome this problem by using different controls on different platforms.

We can override many NativeControlHost methods, but since this is a minimal demo, we’ll override only CreateNativeControlCore. The method should instantiate a native UI control. In our case, we need to instantiate AndroidX.Media3.UI.PlayerView.

Unfortunately, Avalonia UI documentation doesn’t provide a lot of info about this. Luckily, I found an article which I believe will be useful to anyone interested in the topic – Embedding Native (Windows and Linux) Views/Controls/Applications into Avalonia Applications in Easy Samples.

As a starting point for the code below, I used an official Avalonia article – Embed Native Views. Create a basic media player app using Media3 ExoPlayer is another article worth exploring.

Now back to the specific code. CreateNativeControlCore builds the player (IExoPlayer instance) and creates the native UI control (AndroidX.Media3.UI.PlayerView instance). Lastly, the method returns Android handle (AndroidViewControlHandle) to the native control (AndroidX.Media3.UI.PlayerView).

public class ExoPlayerControl : NativeControlHost
{
	public IExoPlayer MainExoPlayer { get; private set; }

	protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
	{
		var parentContext = (parent as AndroidViewControlHandle)?.View.Context ?? global::Android.App.Application.Context;

		// Create player
		ExoPlayerBuilder exoPlayerBuilder = new(parentContext);
		MainExoPlayer = exoPlayerBuilder.Build();

		// Create player view
		AndroidX.Media3.UI.PlayerView exoPlayerView = new(parentContext)
		{
			Player = MainExoPlayer
		};

		return new AndroidViewControlHandle(exoPlayerView);
	}
}

ExoPlayerService.cs

As you can see below, CreateControl method is super simple, but there’s something important you should know. It is not needed only to instantiate an ExoPlayerControl object in order to obtain the handle to the underlying native control. It is necessary that the control gets added to the visual tree. This cannot be done in CreateControl, but somewhere in the main/common project.

To be specific, CreateNativeControlCore (ExoPlayerControl) will not be executed until the control is added to the visual tree. That’s why we have a handler for AttachedToVisualTree event. We use the handler to access the MainExoPlayer object. Doing this in CreateControl would result in having a null value.

Play method is straightforward. We create the URI media (MediaItem), add media to the player and initiate the playback.

public class ExoPlayerService : INativeMediaPlayerService
{
    private IExoPlayer MainExoPlayer { get; set; }

    public Control CreateControl()
    {
        ExoPlayerControl exoPlayerControl = new();
        exoPlayerControl.AttachedToVisualTree += ExoPlayerControl_AttachedToVisualTree;

        return exoPlayerControl;
    }

    private void ExoPlayerControl_AttachedToVisualTree(object sender, Avalonia.VisualTreeAttachmentEventArgs e)
    {
        if (sender is ExoPlayerControl exoPlayerControl)
        {
            MainExoPlayer = exoPlayerControl.MainExoPlayer;
        }
    }

    public void Play(string uri)
    {
        // Create media item
        MediaItem mediaItem = MediaItem.FromUri(uri);

        // Play media item
        MainExoPlayer.ClearMediaItems();
        MainExoPlayer.AddMediaItem(mediaItem);
        MainExoPlayer.Prepare();
        MainExoPlayer.Play();
    }
}

Step 18 – The source code – Main project

The only thing left is to use the native media player service we just built.

XAML

We have the greeting text block, media URI text box, play button and stack panel which acts as a container for native video player control.

<StackPanel
	Orientation="Vertical"
	HorizontalAlignment="Center">

	<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

	<TextBox
		x:Name="MediaURI"
		HorizontalAlignment="Center"
		Text="https://github.com/rafaelreis-hotmart/Audio-Sample-files/raw/refs/heads/master/sample.mp4" />

	<Button
		HorizontalAlignment="Center"
		Content="Play"
		Click="Button_Click" />

	<StackPanel
		x:Name="VideoContainer">

	</StackPanel>

</StackPanel>

Code behind

In InitMediaPlayer method, we call CreateControl. Based on the running platform (Windows vs. Android), we’ll get either an instance to LibVLC VideoView, or an instance to AndroidX.Media3.UI.PlayerView. We set control width and height, and add the control to the container.

public partial class MainView : UserControl
{
    public MainView()
    {
        InitializeComponent();
        InitMediaPlayer();
    }

    private void InitMediaPlayer()
    {
        Control mediaPlayerControl = App.AppNativeVideoPlayerService.CreateControl();

        mediaPlayerControl.Width = 400;
        mediaPlayerControl.Height = 300;

        VideoContainer.Children.Clear();
        VideoContainer.Children.Add(mediaPlayerControl);
    }

    private void Button_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
    {
        App.AppNativeVideoPlayerService.Play(MediaURI.Text);
    }
}

Step 19 – Run the app on Windows

Finally it’s time to play some media!

Avalonia UI - Native Video Playback - 17 - Executing the final demo on Windows
Avalonia UI – Native Video Playback – Executing the final demo on Windows

Step 20 – Run the app on Android

I used Google Pixel 5 – API 34 Android emulator:

Avalonia UI - Native Video Playback - 18 - Executing the final demo on Android (emulator)
Avalonia UI – Native Video Playback – Executing the final demo on Android (emulator)

Conclusion

We covered many topics in this post, such as:

Please share your suggestions for making the content even more useful. I’d be glad to extend some sections to contain more details, or even write separate posts to address the more complex concepts.