Module 10 - Add a media player
In this module, you'll download streaming video data from YouTube and integrate into the app a media player element that can play it.
You'll also learn how to interact with the media player and stop the video when navigating away from the video player page.
Add YouTube video streaming endpoint API
Open the folder Services/Models and add a file named PlayerModels.cs with the following content:
public partial record Format(string? Url, string? QualityLabel); public partial record StreamingData(List<Format>? Formats); public partial record YoutubeData(StreamingData? StreamingData);
Add a file named IYoutubePlayerEndpoint.cs to the Services folder with the following content:
[Headers( "Content-Type: application/json", "User-Agent", "com.google.android.youtube/17.36.4 (Linux; U; Android 12; GB) gzip")] public interface IYoutubePlayerEndpoint { [Post("/player")] Task<ApiResponse<YoutubeData>> GetStreamData([Body] string data, CancellationToken cancellationToken = default); }
Register Refit endpoint
The previous interface is a Refit service. You will now register it with Refit. Open App.xaml.cs, and add the following Refit client registration right after the previous one you added earlier:
services
.AddRefitClientWithEndpoint<IYoutubeEndpoint, YoutubeEndpointOptions>(
context,
configure: (clientBuilder, options) => clientBuilder
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri(options!.Url!);
httpClient.DefaultRequestHeaders.Add("x-goog-api-key", options.ApiKey);
}))
+ .AddRefitClient<IYoutubePlayerEndpoint>(context);
Update IYoutubeService
interface
Open the IYoutubeService.cs file and add the following method to the interface:
Task<string?> GetVideoSourceUrl(string userId, CancellationToken ct);
Update YoutubeService
to use IYoutubePlayerEndpoint
Open the YoutubeService.cs file
Update the
YoutubeService
Primary Constructor by adding a new parameter of typeIYoutubePlayerEndpoint
:- public class YoutubeService(IYoutubeEndpoint client) : IYoutubeService + public class YoutubeService(IYoutubeEndpoint client, IYoutubePlayerEndpoint playerClient) : IYoutubeService { ... }
Implement the method added to the interface using the following code:
public async Task<string?> GetVideoSourceUrl(string videoId, CancellationToken ct) { var streamVideo = $$""" { "videoId": "{{videoId}}", "context": { "client": { "clientName": "ANDROID_TESTSUITE", "clientVersion": "1.9", "androidSdkVersion": 30, "hl": "en", "gl": "US", "utcOffsetMinutes": 0 } } } """; // Get the available stream data var streamData = await playerClient.GetStreamData(streamVideo, ct); // Get the video stream with the highest video quality var streamWithHighestVideoQuality = streamData.Content?. StreamingData? .Formats? .OrderByDescending(s => s.QualityLabel) .FirstOrDefault(); // Get the stream URL var streamUrl = streamWithHighestVideoQuality?.Url; return streamUrl; }
Utilize IYoutubeService
in VideoDetailsModel
Open the file VideoDetailsModel.cs and add a property of type
IYoutubeService
namedYoutubeService
to it (via the record constructor, like theVideo
property):public partial record VideoDetailsModel(YoutubeVideo Video, IYoutubeService YoutubeService)
Add the following method to the record:
private async ValueTask<MediaSource> GetVideoSource(CancellationToken ct) { var streamUrl = await YoutubeService.GetVideoSourceUrl(Video?.Id, ct) ?? throw new InvalidOperationException("Input stream collection is empty."); // Return the MediaSource using the stream URL return MediaSource.CreateFromUri(new Uri(streamUrl)); }
Replace the
VideoSource
property declaration line with the following:public IFeed<MediaSource> VideoSource => Feed.Async(GetVideoSource);
Update the YoutubeServiceMock
Since we updated the IYoutubeService
interface, we need to update the mock implementation as well.
Open the YoutubeServiceMock.cs file and add the following method to the class:
public Task<string?> GetVideoSourceUrl(string videoId, CancellationToken ct) { return Task.FromResult<string?>(default); }
Add Player to layout
Open the VideoDetailsPage.cs file, and add the following variable to the top of the file (right after class opening):
private MediaPlayerElement? youtubePlayer;
Add the
Assign
extension method to theMediaPlayerElement
, so that it can be accessed outside this scope, as well as theAutoPlay
andSource
extension methods:new MediaPlayerElement() + .Assign(mediaPlayerElement => youtubePlayer = mediaPlayerElement) + .AutoPlay(true) + .Source(() => vm.VideoSource) + .PosterSource(() => vm.Video.Details.Snippet?.Thumbnails?.Medium?.Url!) ...
Pause the video when returning to the search results page
When the user navigates back to the search results page, the media player continues to play the video. To avoid that, implement the OnNavigatingFrom
method in VideoDetailsPage.cs as follows:
protected async override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
youtubePlayer?.MediaPlayer.Pause();
}
Add MediaElement Feature
You can set up the MediaElement UnoFeature in a few ways. During project setup, pick 'Media Element' under 'Features' in the wizard. Or, use the CLI with the right parameter during project creation. Also you can add it manually later in the project file.
Here's how to do it now:
- Open the TubePlayer.csproj file.
- Locate the "UnoFeatures" property within the "PropertyGroup" section.
- Add "MediaElement" to the list of features, as shown in the snippet below:
<UnoFeatures>
<!-- Other features here-->
+ MediaElement;
</UnoFeatures>
Note
MediaPlayerElement
is not yet supported for the unified Skia Desktop target (net8.0-desktop
). You can follow progress in this issue.
Run the app
Run the app to see the media player playing the video from YouTube. Seek, volume, pause, and other controls are built into the media player.
You will notice how the video stops before navigating back to the search results (observe the play button right before the navigation happens), but at the same time, we still want to update the navigation bar image (currently a random picture), the app icon, as well as the splashscreen (which are currently set to the template defaults).
Note
In order to be able to play the media in WASM, because of a CORS issue, you will need to either create a server project (e.g. using YARP) or by using a public proxy (e.g. using CORS Anywhere).
For more information on MediaPlayerElement
support, refer to the MediaPlayerElement documentation.