Skip to content

Add support for local files and package resources to MediaElement #2502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c370fe0
Refactor media artwork handling and async updates
ne0rrmatrix Jan 21, 2025
9c3ca06
Refactor and clean up media manager methods
ne0rrmatrix Feb 10, 2025
dadb767
Fix merge conflict
ne0rrmatrix Feb 10, 2025
8d934c6
Merge branch 'CommunityToolkit:main' into MetaDataSource
ne0rrmatrix Feb 10, 2025
49ad676
Fix incorrect logging
ne0rrmatrix Feb 10, 2025
9347a3a
Merge branch 'MetaDataSource' of https://github.com/ne0rrmatrix/MauiO…
ne0rrmatrix Feb 10, 2025
5618e4d
Merge branch 'main' into MetaDataSource
ne0rrmatrix Feb 12, 2025
193ac8a
Fix incorrect accesibility modifier
ne0rrmatrix Feb 12, 2025
a192ebd
Merge branch 'MetaDataSource' of https://github.com/ne0rrmatrix/MauiO…
ne0rrmatrix Feb 12, 2025
0cd2546
Merge branch 'main' into MetaDataSource
ne0rrmatrix Feb 13, 2025
9492160
Merge branch 'main' into MetaDataSource
ne0rrmatrix Feb 15, 2025
8b5176d
Merge branch 'main' into MetaDataSource
ne0rrmatrix Feb 17, 2025
6fcf06f
Merge branch 'main' into MetaDataSource
ne0rrmatrix Mar 7, 2025
f7b18da
Merge branch 'main' into MetaDataSource
ne0rrmatrix Mar 15, 2025
9bcccbb
Merge branch 'main' into MetaDataSource
ne0rrmatrix Apr 1, 2025
4487557
Fix merge Conflict
ne0rrmatrix May 4, 2025
81ad1a8
Revert merge error in file that was not supposed to be changed
ne0rrmatrix May 4, 2025
47979ef
Merge branch 'main' into MetaDataSource
ne0rrmatrix May 7, 2025
00c5378
Fix another merge error
ne0rrmatrix May 7, 2025
6837979
Fix merge conflict
ne0rrmatrix Jun 20, 2025
fb9ac11
Merge branch 'main' into MetaDataSource
ne0rrmatrix Jun 20, 2025
db57d38
Merge branch 'main' into MetaDataSource
ne0rrmatrix Jun 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
x:Name="MediaElement"
ShouldAutoPlay="True"
Source="{x:Static constants:StreamingVideoUrls.BuckBunny}"
MetadataArtworkUrl="https://lh3.googleusercontent.com/pw/AP1GczNRrebWCJvfdIau1EbsyyYiwAfwHS0JXjbioXvHqEwYIIdCzuLodQCZmA57GADIo5iB3yMMx3t_vsefbfoHwSg0jfUjIXaI83xpiih6d-oT7qD_slR0VgNtfAwJhDBU09kS5V2T5ZML-WWZn8IrjD4J-g=w1792-h1024-s-no-gm"
MetadataArtworkSource="https://lh3.googleusercontent.com/pw/AP1GczNRrebWCJvfdIau1EbsyyYiwAfwHS0JXjbioXvHqEwYIIdCzuLodQCZmA57GADIo5iB3yMMx3t_vsefbfoHwSg0jfUjIXaI83xpiih6d-oT7qD_slR0VgNtfAwJhDBU09kS5V2T5ZML-WWZn8IrjD4J-g=w1792-h1024-s-no-gm"
MetadataTitle="Big Buck Bunny"
MetadataArtist="Blender Foundation"
MediaEnded="OnMediaEnded"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public partial class MediaElementPage : BasePage<MediaElementViewModel>
const string loadLocalResource = "Load Local Resource";
const string resetSource = "Reset Source to null";
const string loadMusic = "Load Music";

const string loadCustomMediaSource = "Load Custom Image Source";
static readonly string saveDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
const string buckBunnyMp4Url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
const string botImageUrl = "https://lh3.googleusercontent.com/pw/AP1GczNRrebWCJvfdIau1EbsyyYiwAfwHS0JXjbioXvHqEwYIIdCzuLodQCZmA57GADIo5iB3yMMx3t_vsefbfoHwSg0jfUjIXaI83xpiih6d-oT7qD_slR0VgNtfAwJhDBU09kS5V2T5ZML-WWZn8IrjD4J-g=w1792-h1024-s-no-gm";
const string hlsStreamTestUrl = "https://mtoczko.github.io/hls-test-streams/test-gap/playlist.m3u8";
const string hal9000AudioUrl = "https://github.com/prof3ssorSt3v3/media-sample-files/raw/master/hal-9000.mp3";
Expand Down Expand Up @@ -166,37 +168,34 @@ void Button_Clicked(object? sender, EventArgs e)
async void ChangeSourceClicked(Object sender, EventArgs e)
{
var result = await DisplayActionSheet("Choose a source", "Cancel", null,
loadOnlineMp4, loadHls, loadLocalResource, resetSource, loadMusic);

MediaElement.Stop();
MediaElement.Source = null;
loadOnlineMp4, loadHls, loadLocalResource, resetSource, loadMusic, loadCustomMediaSource);

switch (result)
{
case loadOnlineMp4:
MediaElement.MetadataTitle = "Big Buck Bunny";
MediaElement.MetadataArtworkUrl = botImageUrl;
MediaElement.MetadataArtworkSource = MediaSource.FromUri(botImageUrl);
MediaElement.MetadataArtist = "Big Buck Bunny Album";
MediaElement.Source =
MediaSource.FromUri(StreamingVideoUrls.BuckBunny);
return;

case loadHls:
MediaElement.MetadataArtist = "HLS Album";
MediaElement.MetadataArtworkUrl = botImageUrl;
MediaElement.MetadataArtworkSource = botImageUrl;
MediaElement.MetadataTitle = "HLS Title";
MediaElement.Source = MediaSource.FromUri(hlsStreamTestUrl);
return;

case resetSource:
MediaElement.MetadataArtworkUrl = string.Empty;
MediaElement.MetadataArtworkSource = string.Empty;
MediaElement.MetadataTitle = string.Empty;
MediaElement.MetadataArtist = string.Empty;
MediaElement.Source = null;
return;

case loadLocalResource:
MediaElement.MetadataArtworkUrl = botImageUrl;
MediaElement.MetadataArtworkSource = MediaSource.FromResource("robot.jpg");
MediaElement.MetadataTitle = "Local Resource Title";
MediaElement.MetadataArtist = "Local Resource Album";

Expand All @@ -218,10 +217,57 @@ async void ChangeSourceClicked(Object sender, EventArgs e)
case loadMusic:
MediaElement.MetadataTitle = "HAL 9000";
MediaElement.MetadataArtist = "HAL 9000 Album";
MediaElement.MetadataArtworkUrl = botImageUrl;
MediaElement.MetadataArtworkSource = botImageUrl;
MediaElement.Source = MediaSource.FromUri(hal9000AudioUrl);
return;
case loadCustomMediaSource:
var fileresult = await PickAndShow(new PickOptions
{
PickerTitle = "Pick a media file",
FileTypes = FilePickerFileType.Images,
});
var fileName = await Savefile(fileresult);
if (fileName is not null)
{
MediaElement.MetadataArtworkSource = MediaSource.FromFile(fileName);
MediaElement.MetadataTitle = "Big Buck Bunny";
MediaElement.MetadataArtist = "Big Buck Bunny Album";
MediaElement.Source =
MediaSource.FromUri(buckBunnyMp4Url);
}

return;
}
}
static async Task<string?> Savefile(FileResult? fileresult)
{
if (fileresult is null)
{
System.Diagnostics.Trace.WriteLine("File result is null");
return null;
}
try
{
using Stream fileStream = await fileresult.OpenReadAsync();
using StreamReader reader = new(fileStream);
var fileName = GetFileName(fileresult.FileName);
using FileStream output = File.Create(fileName);
await fileStream.CopyToAsync(output);
fileStream.Seek(0, SeekOrigin.Begin);
FileStream.Synchronized(output);
return fileName;
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
return null;
}

}
static string GetFileName(string name)
{
var filename = Path.GetFileName(name);
return Path.Combine(saveDirectory, filename);
}

async void ChangeAspectClicked(object? sender, EventArgs e)
Expand Down Expand Up @@ -276,7 +322,7 @@ async void DisplayPopup(object sender, EventArgs e)
{
AndroidViewType = AndroidViewType.SurfaceView,
Source = source,
MetadataArtworkUrl = "dotnet_bot.png",
MetadataArtworkSource = botImageUrl,
ShouldAutoPlay = true,
ShouldShowPlaybackControls = true,
};
Expand All @@ -286,4 +332,24 @@ async void DisplayPopup(object sender, EventArgs e)
popupMediaElement.Stop();
popupMediaElement.Source = null;
}
static async Task<FileResult?> PickAndShow(PickOptions options)
{
try
{
var result = await FilePicker.Default.PickAsync(options);
if (result is not null)
{
using var stream = await result.OpenReadAsync();
var image = ImageSource.FromStream(() => stream);
}

return result;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
}

return null;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public interface IMediaElement : IView, IAsynchronousMediaElementHandler
string MetadataArtist { get; set; }

/// <summary>
/// Gets or sets the artwork Image Url.
/// Gets or sets the artwork Image source.
/// </summary>
string MetadataArtworkUrl { get; set; }
MediaSource? MetadataArtworkSource { get; set; }

/// <summary>
/// Gets the media aspect ratio.
Expand Down
16 changes: 8 additions & 8 deletions src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ public partial class MediaElement : View, IMediaElement, IDisposable
public static readonly BindableProperty MetadataArtistProperty = BindableProperty.Create(nameof(MetadataArtist), typeof(string), typeof(MediaElement), string.Empty);

/// <summary>
/// Backing store for the <see cref="MetadataArtworkUrl"/> property.
/// Backing store for the <see cref="MetadataArtworkSource"/> property.
/// </summary>
public static readonly BindableProperty MetadataArtworkUrlProperty = BindableProperty.Create(nameof(MetadataArtworkUrl), typeof(string), typeof(MediaElement), string.Empty);

public static readonly BindableProperty MetadataArtworkSourceProperty = BindableProperty.Create(nameof(MetadataArtworkSource), typeof(MediaSource), typeof(MediaElement));
readonly WeakEventManager eventManager = new();
readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1);

Expand Down Expand Up @@ -365,13 +365,14 @@ public string MetadataArtist
}

/// <summary>
/// Gets or sets the Artwork Image Url of the media.
/// Gets or sets the Artwork Image Source of the media.
/// This is a bindable property.
/// </summary>
public string MetadataArtworkUrl
[TypeConverter(typeof(MediaSourceConverter))]
public MediaSource? MetadataArtworkSource
{
get => (string)GetValue(MetadataArtworkUrlProperty);
set => SetValue(MetadataArtworkUrlProperty, value);
get => (MediaSource)GetValue(MetadataArtworkSourceProperty);
set => SetValue(MetadataArtworkSourceProperty, value);
}

/// <summary>
Expand Down Expand Up @@ -580,7 +581,6 @@ void ClearTimer()
timer.Stop();
timer = null;
}

void OnSourceChanged(object? sender, EventArgs eventArgs)
{
OnPropertyChanged(SourceProperty.PropertyName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AVFoundation;
using CommunityToolkit.Maui.Views;
using CoreMedia;
using Foundation;
using MediaPlayer;
Expand Down Expand Up @@ -69,40 +70,32 @@ public Metadata(PlatformMediaElement player)
/// </summary>
/// <param name="playerItem"></param>
/// <param name="mediaElement"></param>
public void SetMetadata(AVPlayerItem? playerItem, IMediaElement? mediaElement)
public async Task SetMetadata(AVPlayerItem? playerItem, IMediaElement? mediaElement)
{
if (mediaElement is null)
{
Metadata.ClearNowPlaying();
return;
}
ClearNowPlaying();
var artwork = await MetadataArtworkUrl(mediaElement.MetadataArtworkSource).ConfigureAwait(false);

if (artwork is UIImage image)
{
NowPlayingInfo.Artwork = new(boundsSize: new(320, 240), requestHandler: _ => image);
}
else
{
NowPlayingInfo.Artwork = new(boundsSize: new(0, 0), requestHandler: _ => defaultUIImage);
}
NowPlayingInfo.Title = mediaElement.MetadataTitle;
NowPlayingInfo.Artist = mediaElement.MetadataArtist;
NowPlayingInfo.PlaybackDuration = playerItem?.Duration.Seconds ?? 0;
NowPlayingInfo.IsLiveStream = false;
NowPlayingInfo.PlaybackRate = mediaElement.Speed;
NowPlayingInfo.ElapsedPlaybackTime = playerItem?.CurrentTime.Seconds ?? 0;
NowPlayingInfo.Artwork = new(boundsSize: new(320, 240), requestHandler: _ => GetImage(mediaElement.MetadataArtworkUrl));
MPNowPlayingInfoCenter.DefaultCenter.NowPlaying = NowPlayingInfo;
}

static UIImage GetImage(string imageUri)
{
try
{
if (imageUri.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
{
return UIImage.LoadFromData(NSData.FromUrl(new NSUrl(imageUri))) ?? defaultUIImage;
}
return defaultUIImage;
}
catch
{
return defaultUIImage;
}
}

MPRemoteCommandHandlerStatus SeekCommand(MPRemoteCommandEvent? commandEvent)
{
if (commandEvent is not MPChangePlaybackPositionCommandEvent eventArgs)
Expand Down Expand Up @@ -179,4 +172,70 @@ MPRemoteCommandHandlerStatus ToggleCommand(MPRemoteCommandEvent? commandEvent)

return MPRemoteCommandHandlerStatus.Success;
}

public static async Task<UIImage?> MetadataArtworkUrl(MediaSource? artworkUrl, CancellationToken cancellationToken = default)
{
if (artworkUrl is UriMediaSource uriMediaSource)
{
var uri = uriMediaSource.Uri;
return GetBitmapFromUrl(uri?.AbsoluteUri);
}
else if (artworkUrl is FileMediaSource fileMediaSource)
{
var uri = fileMediaSource.Path;

return await GetBitmapFromFile(uri, cancellationToken).ConfigureAwait(false);
}
else if (artworkUrl is ResourceMediaSource resourceMediaSource)
{
var path = resourceMediaSource.Path;
return await GetBitmapFromResource(path, cancellationToken).ConfigureAwait(false);
}
return null;
}

static async Task<UIImage?> GetBitmapFromFile(string? resource, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(resource))
{
return null;
}
using var fileStream = File.OpenRead(resource);
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;
NSData temp = NSData.FromStream(memoryStream) ?? new NSData();
return UIImage.LoadFromData(temp);
}
static UIImage? GetBitmapFromUrl(string? resource)
{
if (string.IsNullOrEmpty(resource))
{
return null;
}
return UIImage.LoadFromData(NSData.FromUrl(new NSUrl(resource)));
}
static async Task<UIImage?> GetBitmapFromResource(string? resource, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(resource))
{
return null;
}
using var inputStream = await FileSystem.OpenAppPackageFileAsync(resource).ConfigureAwait(false);
using var memoryStream = new MemoryStream();
if (inputStream is null)
{
System.Diagnostics.Trace.TraceInformation($"{inputStream} is null.");
return null;
}
await inputStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;
NSData? nsdata = NSData.FromStream(memoryStream);
if (nsdata is null)
{
System.Diagnostics.Trace.TraceInformation($"{nsdata} is null.");
return null;
}
return UIImage.LoadFromData(nsdata);
}
}
Loading
Loading