Easily add a video reel to your MAUI app (like Instagram)

Easily add a video reel to your MAUI app (like Instagram)

ยท

6 min read

Introduction

Social media apps like Instagram use full-page scrollable video controls called "reels" (named after the device used to store film - or garden hoses) which allow the user to swipe or scroll up and down in order to switch between videos (or other content).

In this blog post, I will demonstrate how you can easily add a feature like a video reel to your own .NET MAUI app in a few simple steps. The idea is that we can scroll up and down and the video that becomes visible will start playing while the previously playing video gets stopped. All we need for this is a CollectionView and the MediaElement from the MAUI Community Toolkit (MCT).

After defining the model, we'll first have a look at how we can accomplish the desired layout, which is a full-page view that allows to swipe up and down and always stops at the next element. Afterwards, we'll implement the automatic play/stop functionality.

This post is based on a Stack Overflow answer that I recently wrote.

As always, you can have a look at the full code in the sample repository on GitHub.

Let's do it!

Model

The model for the reel is quite simple, we just need to create a VideoModel with a few properties, e.g. Title, VideoUri and IsPlaying:

public partial class VideoModel(string title, string videoUri) : ObservableObject
{
    public string Title { get; } = title;
    public string VideoUri { get; } = videoUri;

    [ObservableProperty]
    private bool _isPlaying;
}

The IsPlaying property is an auto-generated observable property, which we will need later on to control the automatic play/stop functionality.

ViewModel

In the ViewModel, we can define several instances of VideoModel using some sample video files that are stored locally in the repository, like so:

public partial class ReelViewModel : ObservableObject
{
    private const string FrogVideo = "https://github.com/ewerspej/maui-samples/blob/main/assets/frog.mp4?raw=true";
    private const string BuckVideo = "https://github.com/ewerspej/maui-samples/blob/main/assets/bigbuckbunny.mp4?raw=true";

    [ObservableProperty]
    private ObservableCollection<VideoModel> _videos;

    public ReelViewModel()
    {
        Videos =
        [
            new VideoModel("First", FrogVideo),
            new VideoModel("Second", BuckVideo),
            new VideoModel("Third", FrogVideo),
            new VideoModel("Fourth", BuckVideo),
            new VideoModel("Fifth", FrogVideo),
            new VideoModel("Sixth", BuckVideo)
        ];
    }
}

Later on, we can then bind to the Videos collection in XAML.

Basic Layout

In order to create the reel, we first need to add a CollectionView inside a Grid. The CollectionView will host the VideoModel instances. We want only a single item to be visible at a time, so we'll add a LinearItemsLayout and give the layout in the DataTemplate the sameHeightRequest as the CollectionView. In order to make the scrolling of the CollectionView stop at the next element, we'll also need to use snap points.

<Grid>

  <CollectionView
    HeightRequest="500"
    HorizontalOptions="Fill"
    VerticalOptions="Center"
    ItemsSource="{Binding Videos}"
    Scrolled="ItemsView_OnScrolled">

    <CollectionView.ItemsLayout>
      <LinearItemsLayout
        Orientation="Vertical"
        SnapPointsType="MandatorySingle"
        SnapPointsAlignment="Center" />
    </CollectionView.ItemsLayout>

    <CollectionView.ItemTemplate>
      <DataTemplate x:DataType="models:VideoModel">
        <Grid
          HeightRequest="500"
          HorizontalOptions="Fill"
          BackgroundColor="LightGreen">

          <Label
            Text="{Binding Title}"
            VerticalOptions="Center"
            HorizontalOptions="Center"
            FontSize="Title" />

        </Grid>
      </DataTemplate>
    </CollectionView.ItemTemplate>

  </CollectionView>

</Grid>

The result should something like this and we can scroll back and forth between the elements, with always only a single item visible at a time:

That's already pretty neat. Next, let's add the video player and the automatic play/stop functionality.

Automatic Play/Stop

Before we can implement the play/stop functionality, we need to install the CommunityToolkit.Maui.MediaElement package from nuget.org and initialize the MediaElement in our MauiProgram class before we can use it:

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

In order to automatically play a video that is hosted in a MediaElement, we need to work with a little trick. Since the MediaElement doesn't expose any bindable and settable IsPlaying property, we need to add that ourselves by extending the class with a BindableProperty:

public class ExtendedMediaElement : MediaElement
{
    public bool IsPlaying
    {
        get => (bool)GetValue(IsPlayingProperty);
        set => SetValue(IsPlayingProperty, value);
    }

    public static readonly BindableProperty IsPlayingProperty = BindableProperty.Create(nameof(IsPlaying), typeof(bool), typeof(ExtendedMediaElement), false, propertyChanged: OnIsPlayingPropertyChanged);

    private static void OnIsPlayingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var mediaElement = (ExtendedMediaElement)bindable;

        if (newValue is true)
        {
            mediaElement.Play();
        }
        else
        {
            mediaElement.Stop();
        }
    }
}

By using the OnIsPlayingPropertyChanged event handler, we can decide whether to play or stop the video whenever the IsPlaying property gets updated (via a binding).

With this in place, we can now add our ExtendedMediaElement to the XAML:

<DataTemplate x:DataType="models:VideoModel">
  <Grid
    HeightRequest="{Binding Height, Source={x:Reference Reels}}"
    HorizontalOptions="Fill">

    <media:ExtendedMediaElement
      Aspect="Fill"
      Source="{Binding VideoUri}"
      ShouldAutoPlay="False"
      ShouldLoopPlayback="True"
      ShouldShowPlaybackControls="False"
      IsPlaying="{Binding IsPlaying}" />

    <Label
      Text="{Binding Title}"
      VerticalOptions="Center"
      HorizontalOptions="Center"
      FontSize="Title" />

  </Grid>
</DataTemplate>

Now, a video is only played and stopped whenever the IsPlaying property of the associated VideoModel instance changes.

However, we're not entirely done yet, one important part is still missing. We want the videos to automatically start and stop whenever an element is scrolled into and out of view. For this, we need to hook up an event handler for the Scrolled event of the CollectionView, where we then update the IsPlaying property of each item in the underlying collection:

<CollectionView
  HeightRequest="{Binding Height, Source={x:Reference Reels}}"
  HorizontalOptions="Fill"
  VerticalOptions="Center"
  ItemsSource="{Binding Videos}"
  Scrolled="ItemsView_OnScrolled">

  <!-- ... -->

</CollectionView>

The event handler lives in the code-behind of our page and iterates through the Videos collection to update the IsPlaying property of the VideoModel instances:

public partial class ReelPage : ContentPage
{
    private readonly ReelViewModel _vm;

    public ReelPage(ReelViewModel vm)
    {
        InitializeComponent();
        BindingContext = _vm = vm;
    }

    private void ItemsView_OnScrolled(object sender, ItemsViewScrolledEventArgs e)
    {
        var itemIndex = e.CenterItemIndex;

        _vm.Videos[itemIndex].IsPlaying = true;

        foreach (var myModel in _vm.Videos)
        {
            if (myModel != _vm.Videos[itemIndex])
            {
                myModel.IsPlaying = false;
            }
        }
    }
}

Almost there. Now, the automatic play/stop should already be working. Let's touch up the XAML a little bit and have a look at the final result next.

Final Full-Page Layout

Last, but not least, in order to have the CollectionView take up the entire available space of the page, we can simply bind the HeightRequest of the CollectionView as well as the Grid in the DataTemplate to the rendered Height of the page to end up with the following final layout:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
  x:Class="MauiSamples.Views.ReelPage"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  xmlns:models="clr-namespace:MauiSamples.Models"
  xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
  xmlns:media="clr-namespace:MauiSamples.CustomControls.Media"
  x:Name="Reels"
  x:DataType="viewModels:ReelViewModel"
  Shell.NavBarIsVisible="False">

  <Grid>

    <CollectionView
      HeightRequest="{Binding Height, Source={x:Reference Reels}}"
      HorizontalOptions="Fill"
      VerticalOptions="Center"
      ItemsSource="{Binding Videos}"
      Scrolled="ItemsView_OnScrolled">

      <CollectionView.ItemsLayout>
        <LinearItemsLayout
          Orientation="Vertical"
          SnapPointsType="MandatorySingle"
          SnapPointsAlignment="Center" />
      </CollectionView.ItemsLayout>

      <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:VideoModel">
          <Grid
            HeightRequest="{Binding Height, Source={x:Reference Reels}}"
            HorizontalOptions="Fill">

            <media:ExtendedMediaElement
              Aspect="AspectFill"
              Source="{Binding VideoUri}"
              ShouldAutoPlay="False"
              ShouldLoopPlayback="True"
              ShouldShowPlaybackControls="False"
              IsPlaying="{Binding IsPlaying}" />

            <Label
              Text="{Binding Title}"
              VerticalOptions="Center"
              HorizontalOptions="Center"
              FontSize="Title" />

          </Grid>
        </DataTemplate>
      </CollectionView.ItemTemplate>

    </CollectionView>

  </Grid>

</ContentPage>

Now, when we run the app, we have a full-page reel control:

๐Ÿ† Fantastic, I'm excited about the result.

Conclusions and next steps

As I demonstrated here, adding a scrollable video control similar to Instagram's reels to your .NET MAUI app is quite simple. A neat addition to this would be to make the entire page fullscreen, but that would be too much for a single blog post. The main focus here was supposed to be on how to combine the CollectionView and the MediaElement to create a reel and that's surprisingly easy to achieve.

Note: I didn't touch on any issues regarding caching or performance in this post. When the list of videos gets very long or when items are incrementally loaded, e.g. when using the infinite scroll capabilities of CollectionView, we might observe performance degradation, which should then be addressed separately.
Thanks to Gerald Versluis for pointing this out.

If you enjoyed this blog post, then follow me on LinkedIn, subscribe to this blog and star the GitHubrepositoryfor this post so you don't miss out on any future posts and developments.

Attributions

Title photo by Markus Spiske on Unsplash

ย