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 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 @@ -17,7 +17,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 @@ -161,34 +163,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);
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 @@ -210,10 +212,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 @@ -250,7 +299,7 @@ void DisplayPopup(object sender, EventArgs e)
{
AndroidViewType = AndroidViewType.SurfaceView,
Source = MediaSource.FromResource("AppleVideo.mp4"),
MetadataArtworkUrl = botImageUrl,
MetadataArtworkSource = botImageUrl,
HeightRequest = 600,
WidthRequest = 600,
ShouldAutoPlay = true,
Expand All @@ -275,4 +324,24 @@ void DisplayPopup(object sender, EventArgs e)
popupMediaElement.Stop();
};
}
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