diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml index f017af2ede..af25af3ab3 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml @@ -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" diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs index 0163e3e02f..a97dfb28d2 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs @@ -15,7 +15,9 @@ public partial class MediaElementPage : BasePage 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"; @@ -166,16 +168,13 @@ 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); @@ -183,20 +182,20 @@ async void ChangeSourceClicked(Object sender, EventArgs e) 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"; @@ -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 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) @@ -278,7 +324,7 @@ async void DisplayPopup(object sender, EventArgs e) HeightRequest = 400, AndroidViewType = AndroidViewType.SurfaceView, Source = source, - MetadataArtworkUrl = botImageUrl, + MetadataArtworkSource = botImageUrl, ShouldAutoPlay = true, ShouldShowPlaybackControls = true, }; @@ -288,4 +334,24 @@ async void DisplayPopup(object sender, EventArgs e) popupMediaElement.Stop(); popupMediaElement.Source = null; } + static async Task 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; + } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Resources/Raw/robot.jpg b/samples/CommunityToolkit.Maui.Sample/Resources/Raw/robot.jpg new file mode 100644 index 0000000000..d531def8d2 Binary files /dev/null and b/samples/CommunityToolkit.Maui.Sample/Resources/Raw/robot.jpg differ diff --git a/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs index a8f7ea3ebb..fcff95812b 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs @@ -18,9 +18,9 @@ public interface IMediaElement : IView, IAsynchronousMediaElementHandler string MetadataArtist { get; set; } /// - /// Gets or sets the artwork Image Url. + /// Gets or sets the artwork Image source. /// - string MetadataArtworkUrl { get; set; } + MediaSource? MetadataArtworkSource { get; set; } /// /// Gets the media aspect ratio. diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs index 6d663135f2..f56b2b7862 100644 --- a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs +++ b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs @@ -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); /// - /// Backing store for the property. + /// Backing store for the property. /// - 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); @@ -365,13 +365,14 @@ public string MetadataArtist } /// - /// 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. /// - 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); } /// @@ -580,7 +581,6 @@ void ClearTimer() timer.Stop(); timer = null; } - void OnSourceChanged(object? sender, EventArgs eventArgs) { OnPropertyChanged(SourceProperty.PropertyName); diff --git a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs index 3ce3c627a1..f0e4fc14ad 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs @@ -1,4 +1,5 @@ using AVFoundation; +using CommunityToolkit.Maui.Views; using CoreMedia; using Foundation; using MediaPlayer; @@ -69,40 +70,32 @@ public Metadata(PlatformMediaElement player) /// /// /// - 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) @@ -179,4 +172,70 @@ MPRemoteCommandHandlerStatus ToggleCommand(MPRemoteCommandEvent? commandEvent) return MPRemoteCommandHandlerStatus.Success; } + + public static async Task 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 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 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); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs b/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs deleted file mode 100644 index 47180da2e7..0000000000 --- a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Windows.Media; - -namespace CommunityToolkit.Maui.Core.Primitives; - -sealed class Metadata -{ - readonly IMediaElement? mediaElement; - readonly SystemMediaTransportControls? systemMediaControls; - readonly IDispatcher dispatcher; - /// - /// Initializes a new instance of the class. - /// - public Metadata(SystemMediaTransportControls systemMediaTransportControls, IMediaElement MediaElement, IDispatcher Dispatcher) - { - mediaElement = MediaElement; - this.dispatcher = Dispatcher; - systemMediaControls = systemMediaTransportControls; - systemMediaControls.ButtonPressed += OnSystemMediaControlsButtonPressed; - } - - - void OnSystemMediaControlsButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args) - { - if (mediaElement is null) - { - return; - } - - if (args.Button == SystemMediaTransportControlsButton.Play) - { - if (dispatcher.IsDispatchRequired) - { - dispatcher.Dispatch(() => mediaElement.Play()); - } - else - { - mediaElement.Play(); - } - } - else if (args.Button == SystemMediaTransportControlsButton.Pause) - { - if (dispatcher.IsDispatchRequired) - { - dispatcher.Dispatch(() => mediaElement.Pause()); - } - else - { - mediaElement.Pause(); - } - } - } - - /// - /// Sets the metadata for the given MediaElement. - /// - public void SetMetadata(IMediaElement mp) - { - if (systemMediaControls is null || mediaElement is null) - { - return; - } - - if (!string.IsNullOrEmpty(mp.MetadataArtworkUrl)) - { - systemMediaControls.DisplayUpdater.Thumbnail = Windows.Storage.Streams.RandomAccessStreamReference.CreateFromUri(new Uri(mp.MetadataArtworkUrl ?? string.Empty)); - } - systemMediaControls.DisplayUpdater.Type = MediaPlaybackType.Music; - systemMediaControls.DisplayUpdater.MusicProperties.Artist = mp.MetadataTitle; - systemMediaControls.DisplayUpdater.MusicProperties.Title = mp.MetadataArtist; - systemMediaControls.DisplayUpdater.Update(); - } -} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs index 33a1e0b6ef..cf1de8c82e 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Android.Content; +using Android.Graphics; using Android.Views; using Android.Widget; using AndroidX.Media3.Common; @@ -13,7 +14,6 @@ using CommunityToolkit.Maui.Services; using CommunityToolkit.Maui.Views; using Microsoft.Extensions.Logging; -using AudioAttributes = AndroidX.Media3.Common.AudioAttributes; using DeviceInfo = AndroidX.Media3.Common.DeviceInfo; using MediaMetadata = AndroidX.Media3.Common.MediaMetadata; @@ -596,7 +596,7 @@ static async Task GetBytesFromMetadataArtworkUrl(string url, Cancellatio } catch (Exception e) { - Trace.WriteLine($"Unable to retrieve {nameof(MediaElement.MetadataArtworkUrl)} for {url}.{e}\n"); + Trace.WriteLine($"Unable to retrieve {nameof(MediaElement.MetadataArtworkSource)} for {url}.{e}\n"); return []; } finally @@ -608,7 +608,7 @@ static async Task GetBytesFromMetadataArtworkUrl(string url, Cancellatio } } - static string NormalizeFilePath(string filePath) => filePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + static string NormalizeFilePath(string filePath) => filePath.Replace('\\', System.IO.Path.DirectorySeparatorChar).Replace('/', System.IO.Path.DirectorySeparatorChar); static async ValueTask GetByteCountFromStream(Stream stream, CancellationToken token) { @@ -688,7 +688,7 @@ void StopService(in BoundServiceConnection boundServiceConnection) var path = resourceMediaSource.Path; if (!string.IsNullOrWhiteSpace(path)) { - var assetFilePath = $"asset://{package}{Path.PathSeparator}{path}"; + var assetFilePath = $"asset://{package}{System.IO.Path.PathSeparator}{path}"; return await CreateMediaItem(assetFilePath, cancellationToken).ConfigureAwait(false); } @@ -706,11 +706,8 @@ void StopService(in BoundServiceConnection boundServiceConnection) MediaMetadata.Builder mediaMetaData = new(); mediaMetaData.SetArtist(MediaElement.MetadataArtist); mediaMetaData.SetTitle(MediaElement.MetadataTitle); - var data = await GetBytesFromMetadataArtworkUrl(MediaElement.MetadataArtworkUrl, cancellationToken).ConfigureAwait(true); - if (data is not null && data.Length > 0) - { - mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover); - } + var data = await GetImageFromMediaSource(MediaElement.MetadataArtworkSource, cancellationToken).ConfigureAwait(true) ?? BlankByteArray(); + mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover); mediaItem = new MediaItem.Builder(); mediaItem.SetUri(url); @@ -748,23 +745,104 @@ public void OnTimelineChanged(Timeline? timeline, int reason) { } public void OnTrackSelectionParametersChanged(TrackSelectionParameters? trackSelectionParameters) { } public void OnTracksChanged(Tracks? tracks) { } #endregion - - static class PlaybackState + static async Task GetImageFromMediaSource(MediaSource? mediaSource, CancellationToken cancellationToken = default) + { + if (mediaSource is null) + { + return null; + } + var artworkUrl = mediaSource; + if (artworkUrl is UriMediaSource uriMediaSource) + { + var uri = uriMediaSource.Uri; + if (string.IsNullOrWhiteSpace(uri?.AbsoluteUri)) + { + System.Diagnostics.Trace.TraceInformation("Arkwork Uri is null or empty"); + return null; + } + return await GetBytesFromMetadataArtworkUrl(uri.AbsoluteUri, cancellationToken).ConfigureAwait(false); + } + else if (artworkUrl is FileMediaSource fileMediaSource) + { + var filePath = fileMediaSource.Path; + if(string.IsNullOrWhiteSpace(filePath)) + { + System.Diagnostics.Trace.TraceInformation("Arkwork File path is null or empty"); + return null; + } + return await GetByteArrayFromFile(filePath, cancellationToken).ConfigureAwait(false); + } + else if (artworkUrl is ResourceMediaSource resourceMediaSource) + { + var path = resourceMediaSource.Path; + var item = path?[(path.LastIndexOf('/') + 1)..]; + if (string.IsNullOrWhiteSpace(item)) + { + System.Diagnostics.Trace.TraceInformation("Arkwork Resource path is null or empty"); + return null; + } + return await GetByteArrayFromResource(item).ConfigureAwait(false); + } + return null; + } + + static async Task GetByteArrayFromResource(string resource) { - public const int StateBuffering = 6; - public const int StateConnecting = 8; - public const int StateFailed = 7; - public const int StateFastForwarding = 4; - public const int StateNone = 0; - public const int StatePaused = 2; - public const int StatePlaying = 3; - public const int StateRewinding = 5; - public const int StateSkippingToNext = 10; - public const int StateSkippingToPrevious = 9; - public const int StateSkippingToQueueItem = 11; - public const int StateStopped = 1; - public const int StateError = 7; + // Android file system does not report size of file so we need to use a buffer for the stream. + using var stream = await FileSystem.Current.OpenAppPackageFileAsync(resource); + if(stream is null) + { + return null; + } + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); + var bytes = memoryStream.ToArray(); + return bytes; } - + static async Task GetByteArrayFromFile(string filePath, CancellationToken cancellationToken = default) + { + if(!File.Exists(filePath)) + { + System.Diagnostics.Trace.TraceInformation("Arkwork File does not exist"); + return null; + } + var stream = File.OpenRead(filePath); + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + var bytes = memoryStream.ToArray(); + return bytes; + } + static byte[] BlankByteArray() + { + var bitmapConfig = Bitmap.Config.Argb8888 ?? throw new InvalidOperationException("Bitmap config cannot be null"); + var bitmap = Bitmap.CreateBitmap(1024, 768, bitmapConfig, true); + + Canvas canvas = new(); + canvas.SetBitmap(bitmap); + canvas.DrawColor(Android.Graphics.Color.Transparent); + canvas.Save(); + + MemoryStream stream = new(); + var format = Bitmap.CompressFormat.Png ?? throw new InvalidOperationException("Bitmap format cannot be null"); + bitmap.Compress(format, 100, stream); + stream.Position = 0; + return stream.ToArray(); + } +} +static class PlaybackState +{ + public const int StateBuffering = 6; + public const int StateConnecting = 8; + public const int StateFailed = 7; + public const int StateFastForwarding = 4; + public const int StateNone = 0; + public const int StatePaused = 2; + public const int StatePlaying = 3; + public const int StateRewinding = 5; + public const int StateSkippingToNext = 10; + public const int StateSkippingToPrevious = 9; + public const int StateSkippingToQueueItem = 11; + public const int StateStopped = 1; + public const int StateError = 7; } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs index 352e8fdee0..e0d7798937 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs @@ -210,14 +210,14 @@ protected virtual partial void PlatformUpdateAspect() }; } - protected virtual partial ValueTask PlatformUpdateSource() + protected virtual async partial ValueTask PlatformUpdateSource() { MediaElement.CurrentStateChanged(MediaElementState.Opening); AVAsset? asset = null; if (Player is null) { - return ValueTask.CompletedTask; + return; } metaData ??= new(Player); @@ -265,7 +265,7 @@ protected virtual partial ValueTask PlatformUpdateSource() ? new AVPlayerItem(asset) : null; - metaData.SetMetadata(PlayerItem, MediaElement); + await metaData.SetMetadata(PlayerItem, MediaElement); CurrentItemErrorObserver?.Dispose(); Player.ReplaceCurrentItemWithPlayerItem(PlayerItem); @@ -297,8 +297,7 @@ protected virtual partial ValueTask PlatformUpdateSource() { Player.Play(); } - - SetPoster(); + await SetPoster(); } else if (PlayerItem is null) { @@ -306,52 +305,6 @@ protected virtual partial ValueTask PlatformUpdateSource() MediaElement.CurrentStateChanged(MediaElementState.None); } - - return ValueTask.CompletedTask; - } - - void SetPoster() - { - if (PlayerItem is null || metaData is null) - { - return; - } - - var videoTrack = PlayerItem.Asset.TracksWithMediaType(AVMediaTypes.Video.GetConstant()).FirstOrDefault(); - if (videoTrack is not null) - { - return; - } - - if (PlayerItem.Asset.Tracks.Length == 0) - { - // No video track found and no tracks found. This is likely an audio file. So we can't set a poster. - return; - } - - if (PlayerViewController?.View is not null && PlayerViewController.ContentOverlayView is not null && !string.IsNullOrEmpty(MediaElement.MetadataArtworkUrl)) - { - var image = UIImage.LoadFromData(NSData.FromUrl(new NSUrl(MediaElement.MetadataArtworkUrl))) ?? new UIImage(); - var imageView = new UIImageView(image) - { - ContentMode = UIViewContentMode.ScaleAspectFit, - TranslatesAutoresizingMaskIntoConstraints = false, - ClipsToBounds = true, - AutoresizingMask = UIViewAutoresizing.FlexibleDimensions - }; - - PlayerViewController.ContentOverlayView.AddSubview(imageView); - NSLayoutConstraint.ActivateConstraints( - [ - imageView.CenterXAnchor.ConstraintEqualTo(PlayerViewController.ContentOverlayView.CenterXAnchor), - imageView.CenterYAnchor.ConstraintEqualTo(PlayerViewController.ContentOverlayView.CenterYAnchor), - imageView.WidthAnchor.ConstraintLessThanOrEqualTo(PlayerViewController.ContentOverlayView.WidthAnchor), - imageView.HeightAnchor.ConstraintLessThanOrEqualTo(PlayerViewController.ContentOverlayView.HeightAnchor), - - // Maintain the aspect ratio - imageView.WidthAnchor.ConstraintEqualTo(imageView.HeightAnchor, image.Size.Width / image.Size.Height) - ]); - } } protected virtual partial void PlatformUpdateSpeed() @@ -540,6 +493,51 @@ protected virtual void Dispose(bool disposing) } } + async ValueTask SetPoster(CancellationToken cancellationToken = default) + { + if (PlayerItem is null || metaData is null || MediaElement is null) + { + return; + } + var artwork = await Metadata.MetadataArtworkUrl(MediaElement.MetadataArtworkSource, cancellationToken).ConfigureAwait(false); + if (artwork is null) + { + System.Diagnostics.Trace.TraceInformation($"{artwork} is null."); + return; + } + var videoTrack = PlayerItem.Asset.TracksWithMediaType(AVMediaTypes.Video.GetConstant()).FirstOrDefault(); + if (videoTrack is not null) + { + return; + } + if (PlayerItem.Asset.Tracks.Length == 0) + { + // No video track found and no tracks found. This is likely an audio file. So we can't set a poster. + return; + } + if (PlayerViewController?.View is not null && PlayerViewController.ContentOverlayView is not null) + { + var imageView = new UIImageView(artwork) + { + ContentMode = UIViewContentMode.ScaleAspectFit, + TranslatesAutoresizingMaskIntoConstraints = false, + ClipsToBounds = true, + AutoresizingMask = UIViewAutoresizing.FlexibleDimensions + }; + + PlayerViewController.ContentOverlayView.AddSubview(imageView); + NSLayoutConstraint.ActivateConstraints( + [ + imageView.CenterXAnchor.ConstraintEqualTo(PlayerViewController.ContentOverlayView.CenterXAnchor), + imageView.CenterYAnchor.ConstraintEqualTo(PlayerViewController.ContentOverlayView.CenterYAnchor), + imageView.WidthAnchor.ConstraintLessThanOrEqualTo(PlayerViewController.ContentOverlayView.WidthAnchor), + imageView.HeightAnchor.ConstraintLessThanOrEqualTo(PlayerViewController.ContentOverlayView.HeightAnchor), + + // Maintain the aspect ratio + imageView.WidthAnchor.ConstraintEqualTo(imageView.HeightAnchor, artwork.Size.Width / artwork.Size.Height) + ]); + } + } void AddStatusObservers() { diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs index a62a32b9ad..0e6d67ff33 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using System.Numerics; -using CommunityToolkit.Maui.Core.Primitives; using CommunityToolkit.Maui.Views; using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml.Controls; @@ -8,6 +6,7 @@ using Windows.Media; using Windows.Media.Playback; using Windows.Storage; +using Windows.Storage.Streams; using Windows.System.Display; using ParentWindow = CommunityToolkit.Maui.Extensions.PageExtensions.ParentWindow; using WindowsMediaElement = Windows.Media.Playback.MediaPlayer; @@ -17,7 +16,6 @@ namespace CommunityToolkit.Maui.Core.Views; partial class MediaManager : IDisposable { - Metadata? metadata; SystemMediaTransportControls? systemMediaControls; // States that allow changing position @@ -359,38 +357,64 @@ static bool IsZero(TValue numericValue) where TValue : INumber async ValueTask UpdateMetadata() { - if (systemMediaControls is null || Player is null) + if (systemMediaControls is null || Player is null || MediaElement.MetadataArtworkSource is null) { return; } - metadata ??= new(systemMediaControls, MediaElement, Dispatcher); - metadata.SetMetadata(MediaElement); - if (string.IsNullOrEmpty(MediaElement.MetadataArtworkUrl)) + if (MediaElement.MetadataArtworkSource is UriMediaSource uriMediaSource) { - return; - } - if (!Uri.TryCreate(MediaElement.MetadataArtworkUrl, UriKind.RelativeOrAbsolute, out var metadataArtworkUri)) - { - Trace.TraceError($"{nameof(MediaElement)} unable to update artwork because {nameof(MediaElement.MetadataArtworkUrl)} is not a valid URI"); - return; + var artwork = uriMediaSource.Uri?.AbsoluteUri ?? string.Empty; + var file = RandomAccessStreamReference.CreateFromUri(new Uri(artwork)); + if (file is not null) + { + systemMediaControls.DisplayUpdater.Thumbnail = file; + systemMediaControls.DisplayUpdater.Update(); + Uri uri = new(artwork); + Dispatcher.Dispatch(() => Player.PosterSource = new BitmapImage(uri)); + } } - if (Dispatcher.IsDispatchRequired) + if (MediaElement.MetadataArtworkSource is FileMediaSource fileMediaSource) { - await Dispatcher.DispatchAsync(() => UpdatePosterSource(Player, metadataArtworkUri)); + var artwork = fileMediaSource.Path; + if (File.Exists(artwork)) + { + StorageFile ImageFile = await StorageFile.GetFileFromPathAsync(artwork); + Dispatcher.Dispatch(async () => + { + var bitmap = await LoadBitmapImageAsync(ImageFile); + Player.PosterSource = bitmap; + }); + systemMediaControls.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromFile(ImageFile); + + } } - else + if (MediaElement.MetadataArtworkSource is ResourceMediaSource resourceMediaSource) { - UpdatePosterSource(Player, metadataArtworkUri); + var artwork = "ms-appx:///" + resourceMediaSource.Path; + var file = RandomAccessStreamReference.CreateFromUri(new Uri(artwork)); + if (file is not null) + { + systemMediaControls.DisplayUpdater.Thumbnail = file; + systemMediaControls.DisplayUpdater.Update(); + Uri uri = new(artwork); + Dispatcher.Dispatch(() => Player.PosterSource = new BitmapImage(uri)); + } } - static void UpdatePosterSource(in MediaPlayerElement player, in Uri metadataArtworkUri) - { - player.PosterSource = new BitmapImage(metadataArtworkUri); - } + systemMediaControls.DisplayUpdater.Type = MediaPlaybackType.Music; + systemMediaControls.DisplayUpdater.MusicProperties.Artist = MediaElement.MetadataTitle; + systemMediaControls.DisplayUpdater.MusicProperties.Title = MediaElement.MetadataArtist; + systemMediaControls.DisplayUpdater.Update(); + } + static async Task LoadBitmapImageAsync(StorageFile artwork) + { + var bitmap = new BitmapImage(); + using var randomAccessStream = await artwork.OpenReadAsync(); + await bitmap.SetSourceAsync(randomAccessStream); + return bitmap; } - async void OnMediaElementMediaOpened(WindowsMediaElement sender, object args) { if (Player is null) @@ -505,7 +529,7 @@ void OnPlaybackSessionPlaybackStateChanged(MediaPlaybackSession sender, object a }); } } - + void OnPlaybackSessionSeekCompleted(MediaPlaybackSession sender, object args) { MediaElement?.SeekCompleted(); diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs index 2837c6fd67..e5d6a70d31 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs @@ -16,17 +16,17 @@ public MediaElementTests() public void PosterIsNotStringEmptyOrNull() { MediaElement mediaElement = new(); - mediaElement.MetadataArtworkUrl = "https://www.example.com/image.jpg"; - Assert.False(string.IsNullOrEmpty(mediaElement.MetadataArtworkUrl)); + mediaElement.MetadataArtworkSource = "https://www.example.com/image.jpg"; + Assert.IsType(mediaElement.MetadataArtworkSource, exactMatch: false); + Assert.False((mediaElement.MetadataArtworkSource) is null); } [Fact] public void PosterIsStringEmptyDoesNotThrow() { MediaElement mediaElement = new(); - mediaElement.MetadataArtworkUrl = string.Empty; - Assert.True(string.IsNullOrEmpty(mediaElement.MetadataArtworkUrl)); - Assert.True(mediaElement.MetadataArtworkUrl == string.Empty); + mediaElement.MetadataArtworkSource = null; + Assert.True((mediaElement.MetadataArtworkSource) is null); } [Fact]