Download an audio file in .NET MAUI

β€Ž
To ease your read, please resume from this chapter where we have set up the MediaElement.
Here we go again for a new chapter!

It’s already episode 13 of this series, so I hope it’s still relatively easy to follow! But you may have questions or comments. If you do, ask me in the comments at the bottom of the article, or e-mail me directly (jeanemmanuel.baillat@gmail.com)!

Today, we will have a look at how to enable the user to download the music that is currently playing. We have been listening to the same song over and over, I’m sure you have been dreaming of being able to download it from the app! πŸ˜„

Adding a new ViewModel

First of all, we need to set up a new ViewModel for the MusicPlayerView. To do this, add a new class named MusicPlayerViewModel to the ViewModels folder, and define it with the following code:

Filename:MusicPlayerViewModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace NightClub.ViewModels;

public partial class MusicPlayerViewModel : ObservableObject
{
    #region Properties
    #endregion

    public MusicPlayerViewModel()
    {
    }

    #region Commands
    #endregion
}

β€Ž
If figuring out this bit of code is difficult for you, then don’t get discouraged and take some time to read again the chapter on MVVM.
Of course, this ViewModel doesn’t do anything at the moment, but it’s ready to be associated with its View. So open the file MusicPlayerView.cs and modify it as follows:

Filename:MusicPlayerView.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
// This using is mandatory to resolve the definition of MusicPlayerViewModel
using NightClub.ViewModels;

namespace NightClub.Views;
public class MusicPlayerView : ContentPage
{
    public MusicPlayerView()
    {
        Console.WriteLine("[NightClub] MusicPlayerView - Constructor");

        // Here is where the association is happening
        BindingContext = new MusicPlayerViewModel();

        NavigationPage.SetHasNavigationBar(this, false);
        BackgroundColor = Colors.DimGray;
        ...
    }
    ...
}

As with the HomeViewModel associated with the HomeView, here we have modified the MusicPlayerView’s BindingContext to associate it with the new MusicPlayerViewModel.

Well, that was quick. Now let’s see how to structure application data by defining the Model of music tracks!

A new class for music tracks

Each music track played in the application is defined by a panel of information that we’ll group together in a class called MusicTrack. As you’ll have guessed, this new object is part of our application’s Model.

Start by creating a new folder called Models, then add a new class defined by the following code:

Filename:MusicTrack.cs

1
2
3
4
5
6
7
8
9
namespace NightClub.Models;

public class MusicTrack
{
    public string AudioURL { get; set; }
    public string AudioDownloadURL { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}

For the purposes of this course, 4 string properties are required to contain the following information:

  • The link for streaming audio (AudioURL),

  • The link for downloading audio (AudioDownloadURL),

  • The music track name (Title),

  • And the name of his author (Author).

Since it is now possible to manipulate music tracks, we will add a MusicTrack property to the MusicPlayerViewModel to define the song currently playing:

Filename:MusicPlayerViewModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
using NightClub.Models; // This is required to resolve MusicTrack object!

namespace NightClub.ViewModels;

public partial class MusicPlayerViewModel : ObservableObject
{
    #region Properties

    [ObservableProperty]
    MusicTrack currentTrack;

    #endregion
    ...
}

This perfectly fits in with the Model-View-ViewModel (MVVM) breakdown, since we’ve declared it as an [ObservableProperty], which is an annotation provided by the MVVM Toolkit library.

This annotation will then generate all the code required to trigger events (generally towards the View) in case of a value change. Indeed, we want to match the information displayed in the View to its associated ViewModel.

β€Ž
πŸ’β€Ž β€Ž Got it! But how are we going to define this song?
For the moment, it’s easy because our application only supports the playback of a single song. So we’ll simply initialize it from the MusicPlayerViewModel constructor, as follows:

Filename:MusicPlayerViewModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public MusicPlayerViewModel()
{
    CurrentTrack = new MusicTrack()
    {
        AudioURL = "https://prod-1.storage.jamendo.com/?trackid=1890762&format=mp31&from=b5bSbOTAT1kXawaT8EV9IA%3D%3D%7CGcDX%2BeejT3P%2F0CfPwtSyYA%3D%3D",
        AudioDownloadURL = "https://prod-1.storage.jamendo.com/download/track/1890762/mp32/",
        Author = "Alfonso Lugo",
        Title = "Baila",
    };
}

β€Ž
All the information is provided by Jamendo, a website for free & independent music.
But since we’ve defined the music track inside the MusicPlayerViewModel, we now need to rework the MusicPlayerView to reconfigure the MusicPlayer.

To do this, we need to modify the InitMusicPlayer() method inside the MusicPlayerView, and apply the Data Binding to the Source property of the MediaElement:

Filename:MusicPlayerView.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
using NightClub.Models; // This is required to resolve MusicTrack object!

namespace NightClub.Views;
public class MusicPlayerView : ContentPage
{
    ...
    #region MusicPlayer

    MediaElement MusicPlayer = new MediaElement();

    // And here's the new definition for that method...
    void InitMusicPlayer()
    {
        MusicPlayer.ShouldAutoPlay = true;

        // ... with the binding logic on the MusicPlayer.
        MusicPlayer.Bind(
            MediaElement.SourceProperty,
            nameof(MusicPlayerViewModel.CurrentTrack),
            convert: (MusicTrack musicTrack) => MediaSource.FromUri(musicTrack.AudioURL)
            );
    }

    #endregion
    ...
}

Remember, the MediaElement’s Source property is used to define the source of the media to play. And now, the MusicPlayer.Source property is dynamically linked to the CurrentTrack property defined inside the MusicPlayerViewModel.

Also in the convert, we must not forget to transform the audio streaming link (musicTrack.AudioURL) with the MediaSource.FromUri() method, to conform to the type of the MediaElement’s Source property.

That’s it! Relaunch the project and check that everything is working as before. I wouldn’t want you to be lost in the middle! πŸ˜›

Is everything OK? Then let’s move on quickly to the most interesting part of this chapter: downloading!

Download a song track

Let’s continue our journey by implementing the download button.

To do this, we’re going to associate an action triggered by clicking on the DownloadButton…

Filename:MusicPlayerView.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#region Media Control Panel
    ...
    // The β€œ=>” sign has been replaced with β€œ=”
    ImageButton DownloadButton = new ImageButton
    {
        CornerRadius = 5,
        HeightRequest = 25,
        WidthRequest = 25,
        Source = "download.png",
        BackgroundColor = Colors.Black
    } .BindCommand("DownloadCurrentTrackCommand"); // And here's the command to associate

#endregion

… and whose behavior will be defined inside MusicPlayerViewModel:

Filename:MusicPlayerViewModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
using CommunityToolkit.Maui.Alerts; // This "using" is new...
using CommunityToolkit.Maui.Storage; // ... and this one as well !

namespace NightClub.ViewModels;
public partial class MusicPlayerViewModel : ObservableObject
{
    ...
    #region Commands

    [RelayCommand]
    async Task DownloadCurrentTrack(CancellationToken cancellationToken)
    {
        await Toast
            .Make($"[TEST] You have successfully downloaded \"{CurrentTrack.Title} - {CurrentTrack.Author}\"!")
            .Show(cancellationToken);
    }

    #endregion
}

Remember the [RelayCommand] annotation? We already used it in the MVVM chapter. It allows our method DownloadCurrentTrack() to be called from the View!

And let me stop you right there, the Toast() method has nothing to do with your breakfast πŸ˜„

This method is provided by the .NET MAUI Community Toolkit library, and it can be used as an in-app notification to temporarily display a message on the screen.

At this stage, we can already test that our button is working properly:

Actually, this message is pretty useful!

β€Ž
πŸ’β€Ž β€Ž Are you cheating on me? It doesn’t download anything at all! πŸ˜„
I’ll share the final code with you right after… patience! πŸ€“

Just before, I’d like to bring your attention to the parameter that is required by our new command, the cancellationToken. This is an object of type CancellationToken that keeps a link with the code that initiated the call to the method DownloadCurrentTrack() in the event of a request to cancel its execution.

This is powerful for operations that take a little longer, for example if our user decides to cancel the download due to bad network. We’re not going to implement this feature today, but it would be a great improvement!

Let’s now move on to the download operation itself, with the full implementation below:

Filename:MusicPlayerViewModel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#region Commands

[RelayCommand]
async Task DownloadCurrentTrack(CancellationToken cancellationToken)
{
    // We raise an exception when cancellation is requested
    cancellationToken.ThrowIfCancellationRequested();

    try
    {
        // We need an HTTP client to send our request through the network
        HttpClient client = new HttpClient();
        client.MaxResponseContentBufferSize = 100000000; // We can download up to ~100MB of data per file!

        // We send an HTTP request to the link for downloading audio
        using var httpResponse =
            await client.GetAsync(
                new Uri(CurrentTrack.AudioDownloadURL), cancellationToken);

        httpResponse.EnsureSuccessStatusCode();

        var downloadedImage = await httpResponse.Content.ReadAsStreamAsync(cancellationToken);

        try
        {
            string fileName = $"{CurrentTrack.Title} - {CurrentTrack.Author}.mp3";

            // The retrieved data is then transferred to a file
            // Note: we need CommunityToolkit.Maui to be updated to 5.1.0 at least
            var fileSaveResult = await FileSaver.SaveAsync(fileName, downloadedImage, cancellationToken);

            fileSaveResult.EnsureSuccess();

            await Toast.Make($"File saved at: {fileSaveResult.FilePath}").Show(cancellationToken);
        }
        catch (Exception ex)
        {
            await Toast.Make($"Cannot save file because: {ex.Message}").Show(cancellationToken);
        }
    }
    catch (Exception ex)
    {
        await Toast.Make($"Cannot download file because: {ex.Message}").Show(cancellationToken);
    }
}

#endregion

This is a big piece of code, but nothing too complicated!

Let’s walkthrough step by step:

  1. We first define an HTTP client to enable us to make a request to the download link of the currently playing track (CurrentTrack.AudioDownloadURL),

  2. In return, we expect a positive response from the server to provide us with the corresponding data,

  3. And then, if everything’s fine, we open a read channel to transfer the data to a file and request that it be saved on the device.

As you can see, there’s really no complex logic here. It’s just a bit technical! So, as always, take some time to explore the subject if you need to.

β€Ž
Going further with HTTP request management.
As you’ll have noticed, saving the audio file is made possible by FileSaver. This feature is provided as of version 5.1.0 of the CommunityToolkit.Maui library, and requires a few more configurations to target Android.

To do this, go to the Platforms folder, then Android, and open the file AndroidManifest.xml to add the following lines:

Filename:AndroidManifest.xml

1
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Finally, we’ve coded a few messages to help the user understand what’s going on in the background. After all, we wouldn’t want the user to wait indefinitely because of an error that occurred during the download process! And as these are only informal messages, with no action required, I preferred to use the famous Toast to display ephemeral notifications.

And that’s it. Give it a try!

This time, the download is real!

Congratulations on all your hard work! One last effort and manipulation of media will hold no secrets for you. See you in the next chapter to manage the music playlist!


More articles in the series:

0%