diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml index 197fae8198..83a25d9980 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 a3c25c7f37..b37db46a05 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,7 @@ 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 loadFromFile = "Load from File"; 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,7 +166,7 @@ await DisplayAlertAsync("Error Loading URL Source", "No value was found to load async void ChangeSourceClicked(object? sender, EventArgs? e) { var result = await DisplayActionSheetAsync("Choose a source", "Cancel", null, - loadOnlineMp4, loadHls, loadLocalResource, resetSource, loadMusic); + loadOnlineMp4, loadHls, loadLocalResource, resetSource, loadMusic, loadFromFile); MediaElement.Stop(); MediaElement.Source = null; @@ -175,7 +175,7 @@ async void ChangeSourceClicked(object? sender, EventArgs? e) { case loadOnlineMp4: MediaElement.MetadataTitle = "Big Buck Bunny"; - MediaElement.MetadataArtworkUrl = botImageUrl; + MediaElement.MetadataArtworkSource = botImageUrl; MediaElement.MetadataArtist = "Big Buck Bunny Album"; MediaElement.Source = MediaSource.FromUri(StreamingVideoUrls.BuckBunny); @@ -183,20 +183,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 = null; 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,9 +218,32 @@ 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 loadFromFile: + var fileResult = await PickAndShow(new PickOptions + { + FileTypes = FilePickerFileType.Images, + PickerTitle = "Please select an image file" + }); + if (fileResult is not null) + { + MediaElement.MetadataArtworkSource = MediaSource.FromFile(fileResult.FullPath); + } + MediaElement.MetadataTitle = "Downloaded file"; + MediaElement.MetadataArtist = "From File Album"; + + fileResult = await PickAndShow(new PickOptions + { + FileTypes = FilePickerFileType.Videos, + PickerTitle = "Please select a video file" + }); + if (fileResult is not null) + { + MediaElement.Source = MediaSource.FromFile(fileResult.FullPath); + } + return; } } @@ -278,7 +301,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 +311,20 @@ async void DisplayPopup(object? sender, EventArgs? e) popupMediaElement.Stop(); popupMediaElement.Source = null; } + + async Task PickAndShow(PickOptions options) + { + try + { + var result = await FilePicker.Default.PickAsync(options); + return result; + } + catch (Exception ex) + { + // The user canceled or something went wrong + logger.LogError(ex, "Error picking file"); + } + + 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 ddd454029a..d43e5d8698 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs @@ -18,6 +18,11 @@ public interface IMediaElement : IView, IAsynchronousMediaElementHandler /// event EventHandler PositionChanged; + /// + /// Gets or sets the artwork Image source. + /// + MediaSource? MetadataArtworkSource { get; set; } + /// /// Gets the media aspect ratio. /// @@ -106,11 +111,6 @@ public interface IMediaElement : IView, IAsynchronousMediaElementHandler /// string MetadataArtist { get; set; } - /// - /// Gets or sets the artwork Image Url. - /// - string MetadataArtworkUrl { get; set; } - /// /// Occurs when the media has ended playing successfully. /// diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs index 36e41b7740..89a1c8fb16 100644 --- a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs +++ b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs @@ -117,9 +117,9 @@ public partial class MediaElement : View, IMediaElement, IDisposable public static readonly BindableProperty MetadataArtistProperty = BindableProperty.Create(nameof(MetadataArtist), typeof(string), typeof(MediaElement), MediaElementDefaults.MetadataArtist); /// - /// Bindable property for the property. + /// Bindable property for the property. /// - public static readonly BindableProperty MetadataArtworkUrlProperty = BindableProperty.Create(nameof(MetadataArtworkUrl), typeof(string), typeof(MediaElement), MediaElementDefaults.MetadataArtworkUrl); + public static readonly BindableProperty MetadataArtworkSourceProperty = BindableProperty.Create(nameof(MetadataArtworkSource), typeof(MediaSource), typeof(MediaElement), MediaElementDefaults.MetadataArtworkSource); readonly WeakEventManager eventManager = new(); readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1); @@ -333,14 +333,14 @@ public string MetadataArtist } /// - /// Gets or sets the of the media. + /// Gets or sets the of the media. /// - 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); } - /// /// Gets or sets the of the media. /// @@ -577,7 +577,6 @@ void ClearTimer() timer.Stop(); timer = null; } - void OnSourceChanged(object? sender, EventArgs eventArgs) { OnPropertyChanged(SourceProperty.PropertyName); diff --git a/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs b/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs index a1105823a8..90dab21623 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs @@ -1,3 +1,5 @@ +using CommunityToolkit.Maui.Views; + namespace CommunityToolkit.Maui.Core; static class MediaElementDefaults @@ -26,7 +28,7 @@ static class MediaElementDefaults public const string MetadataArtist = ""; - public const string MetadataArtworkUrl = ""; + public static MediaSource? MetadataArtworkSource => null; public const MediaElementState CurrentState = MediaElementState.None; diff --git a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs index 4d96128186..e9124eb210 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,42 +70,33 @@ public Metadata(PlatformMediaElement player) /// /// /// - public void SetMetadata(AVPlayerItem? playerItem, IMediaElement? mediaElement) + /// + public async Task SetMetadata(AVPlayerItem? playerItem, IMediaElement? mediaElement, CancellationToken cancellationToken = default) { if (mediaElement is null) { - Metadata.ClearNowPlaying(); return; } + ClearNowPlaying(); + var artwork = await MetadataArtworkMediaSource(mediaElement.MetadataArtworkSource, cancellationToken).ConfigureAwait(false); - var url = mediaElement.MetadataArtworkUrl; - + 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(url)); 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) @@ -181,4 +173,111 @@ MPRemoteCommandHandlerStatus ToggleCommand(MPRemoteCommandEvent? commandEvent) return MPRemoteCommandHandlerStatus.Success; } + + public static async Task MetadataArtworkMediaSource(MediaSource? artworkUrl, CancellationToken cancellationToken = default) + { + switch(artworkUrl) + { + case UriMediaSource uriMediaSource: + var uri = uriMediaSource.Uri; + return GetBitmapFromUrl(uri?.AbsoluteUri); + case FileMediaSource fileMediaSource: + var uriFile = fileMediaSource.Path; + return await GetBitmapFromFile(uriFile, cancellationToken).ConfigureAwait(false); + case ResourceMediaSource resourceMediaSource: + var path = resourceMediaSource.Path; + return await GetBitmapFromResource(path, cancellationToken).ConfigureAwait(false); + case null: + return null; + } + return null; + } + + static async Task GetBitmapFromFile(string? resource, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(resource)) + { + System.Diagnostics.Trace.WriteLine("Metadata artwork file path is null or empty."); + return null; + } + if (!File.Exists(resource)) + { + System.Diagnostics.Trace.WriteLine($"Metadata artwork file not found: '{resource}'."); + return null; + } + try + { + 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); + } + catch (IOException ex) + { + System.Diagnostics.Trace.WriteLine($"Error reading metadata artwork file '{resource}': {ex}"); + return null; + } + catch (UnauthorizedAccessException ex) + { + System.Diagnostics.Trace.WriteLine($"Access denied reading metadata artwork file '{resource}': {ex}"); + return null; + } + } + + static UIImage? GetBitmapFromUrl(string? resource) + { + if (string.IsNullOrEmpty(resource)) + { + return null; + } + + try + { + var nsUrl = new NSUrl(resource); + NSData? data = NSData.FromUrl(nsUrl); + if (data is null) + { + System.Diagnostics.Trace.WriteLine($"Failed to load metadata artwork from URL: '{resource}' - NSData.FromUrl returned null."); + return null; + } + + UIImage? image = UIImage.LoadFromData(data); + if (image is null) + { + System.Diagnostics.Trace.WriteLine($"Failed to create UIImage from URL: '{resource}' - UIImage.LoadFromData returned null."); + } + + return image; + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine($"Error loading metadata artwork from URL '{resource}': {ex}"); + return null; + } + } + 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.WriteLine($"Failed to open app package file: '{resource}' - stream 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 create from stream: {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 9b71a68ee9..0000000000 --- a/src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.Maui.Dispatching; -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 ce5f3174ef..688ed7ce06 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Android.App; using Android.Content; -using Android.Util; using Android.Views; using Android.Widget; using AndroidX.Media3.Common; @@ -15,7 +12,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; @@ -27,13 +23,13 @@ public partial class MediaManager : Java.Lang.Object, IPlayerListener const int readyState = 3; const int endedState = 4; - static readonly HttpClient client = new(); + readonly HttpClient client = new(); readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1); - bool isAndroidForegroundServiceEnabled = false; double? previousSpeed; float volumeBeforeMute = 1; + bool isAndroidForegroundServiceEnabled = false; TaskCompletionSource? seekToTaskCompletionSource; CancellationTokenSource? cancellationTokenSource; MediaSession? session; @@ -137,6 +133,7 @@ or PlaybackState.StateSkippingToQueueItem Player = new ExoPlayerBuilder(MauiContext.Context).Build() ?? throw new InvalidOperationException("Player cannot be null"); Player.AddListener(this); this.isAndroidForegroundServiceEnabled = isAndroidServiceEnabled; + if (androidViewType is AndroidViewType.SurfaceView) { PlayerView = new PlayerView(MauiContext.Context) @@ -158,7 +155,7 @@ or PlaybackState.StateSkippingToQueueItem var xmlResource = resources.GetXml(Microsoft.Maui.Resource.Layout.textureview); xmlResource.Read(); - var attributes = Xml.AsAttributeSet(xmlResource)!; + var attributes = Android.Util.Xml.AsAttributeSet(xmlResource)!; PlayerView = new PlayerView(MauiContext.Context, attributes) { @@ -365,16 +362,17 @@ protected virtual async partial ValueTask PlatformUpdateSource() Player.ClearMediaItems(); MediaElement.Duration = TimeSpan.Zero; MediaElement.CurrentStateChanged(MediaElementState.None); - + Player.SetMediaItem(null); + UpdateNotifications(); return; } MediaElement.CurrentStateChanged(MediaElementState.Opening); Player.PlayWhenReady = MediaElement.ShouldAutoPlay; - cancellationTokenSource ??= new(); - // ConfigureAwait(true) is required to prevent crash on startup - var result = await SetPlayerData(cancellationTokenSource.Token).ConfigureAwait(true); - var item = result?.Build(); + var source = GetSource(MediaElement.Source); + cancellationTokenSource = new CancellationTokenSource(); + var result = await CreateMediaItem(source, cancellationTokenSource.Token); + var item = result.Build(); if (item?.MediaMetadata is not null) { @@ -545,11 +543,118 @@ protected override void Dispose(bool disposing) } } - static async Task GetBytesFromMetadataArtworkUrl(string url, CancellationToken cancellationToken = default) + void StartService() + { + if (!isAndroidForegroundServiceEnabled) + { + return; + } + var intent = new Intent(Android.App.Application.Context, typeof(MediaControlsService)); + connection = new BoundServiceConnection(this); + connection.MediaControlsServiceTaskRemoved += HandleMediaControlsServiceTaskRemoved; + + Android.App.Application.Context.StartForegroundService(intent); + Android.App.Application.Context.ApplicationContext?.BindService(intent, connection, Bind.AutoCreate); + } + + void StopService(in BoundServiceConnection boundServiceConnection) + { + if (!isAndroidForegroundServiceEnabled) + { + return; + } + boundServiceConnection.MediaControlsServiceTaskRemoved -= HandleMediaControlsServiceTaskRemoved; + + var serviceIntent = new Intent(Platform.AppContext, typeof(MediaControlsService)); + Android.App.Application.Context.StopService(serviceIntent); + Platform.AppContext.UnbindService(boundServiceConnection); + } + + void HandleMediaControlsServiceTaskRemoved(object? sender, EventArgs e) => Player?.Stop(); + + string? GetSource(MediaSource? mediaSource) + { + if (mediaSource is null) + { + return null; + } + + switch (mediaSource) + { + case UriMediaSource uriMediaSource: + return uriMediaSource.Uri?.AbsoluteUri; + case FileMediaSource fileMediaSource: + return fileMediaSource.Path; + case ResourceMediaSource resourceMediaSource: + { + var package = PlayerView?.Context?.PackageName ?? ""; + var path = resourceMediaSource.Path; + if (!string.IsNullOrWhiteSpace(path)) + { + return $"asset://{package}{System.IO.Path.PathSeparator}{path}"; + } + + break; + } + default: + throw new NotSupportedException($"{mediaSource?.GetType().FullName} is not a supported MediaSource type"); + } + return null; + } + async Task CreateMediaItem(string? url, CancellationToken cancellationToken) + { + MediaMetadata.Builder mediaMetaData = new(); + mediaMetaData.SetArtist(MediaElement.MetadataArtist); + mediaMetaData.SetTitle(MediaElement.MetadataTitle); + var data = await GetArtworkFromMediasource(MediaElement.MetadataArtworkSource, cancellationToken); + mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover); + + mediaItem = new MediaItem.Builder(); + mediaItem.SetUri(url); + mediaItem.SetMediaId(url); + mediaItem.SetMediaMetadata(mediaMetaData.Build()); + + return mediaItem; + } + + async Task GetArtworkFromMediasource(MediaSource? mediaSource, CancellationToken cancellationToken = default) + { + if (mediaSource is null) + { + return null; + } + switch (mediaSource) + { + case FileMediaSource fileMediaSource: + var filePath = fileMediaSource.Path; + if (filePath is null || string.IsNullOrWhiteSpace(filePath)) + { + return null; + } + return await GetByteArrayFromFile(filePath, cancellationToken).ConfigureAwait(false); + case ResourceMediaSource resourceMediaSource: + var resource = resourceMediaSource.Path; + if (resource is null || string.IsNullOrWhiteSpace(resource)) + { + return null; + } + return await GetMauiAssetBytes(resource, cancellationToken).ConfigureAwait(false); + case UriMediaSource uriMediaSource: + var url = uriMediaSource.Uri?.AbsoluteUri; + if (url is null || string.IsNullOrWhiteSpace(url)) + { + return null; + } + return await GetBytesFromMetadataArtworkUrl(url, cancellationToken).ConfigureAwait(false); + default: return null; + } + } + + async Task GetBytesFromMetadataArtworkUrl(string url, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(url)) { - return []; + return null; } Stream? stream = null; @@ -571,22 +676,6 @@ static async Task GetBytesFromMetadataArtworkUrl(string url, Cancellatio var response = await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); stream = response.IsSuccessStatusCode ? await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) : null; } - // Absolute File Path - else if (uri is not null && uri.Scheme == Uri.UriSchemeFile) - { - var normalizedFilePath = NormalizeFilePath(url); - - stream = File.Open(normalizedFilePath, FileMode.Create); - contentLength = await GetByteCountFromStream(stream, cancellationToken); - } - // Relative File Path - else if (Uri.TryCreate(url, UriKind.Relative, out _)) - { - var normalizedFilePath = NormalizeFilePath(url); - - stream = Platform.AppContext.Assets?.Open(normalizedFilePath) ?? throw new InvalidOperationException("Assets cannot be null"); - contentLength = await GetByteCountFromStream(stream, cancellationToken); - } if (stream is not null) { @@ -604,8 +693,8 @@ static async Task GetBytesFromMetadataArtworkUrl(string url, Cancellatio } catch (Exception e) { - Trace.WriteLine($"Unable to retrieve {nameof(MediaElement.MetadataArtworkUrl)} for {url}.{e}\n"); - return []; + System.Diagnostics.Trace.WriteLine($"Unable to retrieve {nameof(MediaElement.MetadataArtworkSource)} for {url}.{e}\n"); + return null; } finally { @@ -615,124 +704,31 @@ static async Task GetBytesFromMetadataArtworkUrl(string url, Cancellatio await stream.DisposeAsync(); } } - - static string NormalizeFilePath(string filePath) => filePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); - - static async ValueTask GetByteCountFromStream(Stream stream, CancellationToken token) - { - if (stream.CanSeek) - { - return stream.Length; - } - - long countedStreamBytes = 0; - - var buffer = new byte[8192]; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(buffer, token)) > 0) - { - countedStreamBytes += bytesRead; - } - - return countedStreamBytes; - } } - void StartService() + static async Task GetByteArrayFromFile(string filePath, CancellationToken cancellationToken = default) { - if (!isAndroidForegroundServiceEnabled) - { - return; - } - var intent = new Intent(global::Android.App.Application.Context, typeof(MediaControlsService)); - connection = new BoundServiceConnection(this); - connection.MediaControlsServiceTaskRemoved += HandleMediaControlsServiceTaskRemoved; - - global::Android.App.Application.Context.StartForegroundService(intent); - global::Android.App.Application.Context.ApplicationContext?.BindService(intent, connection, Bind.AutoCreate); - } - - void StopService(in BoundServiceConnection boundServiceConnection) - { - if (!isAndroidForegroundServiceEnabled) - { - return; - } - boundServiceConnection.MediaControlsServiceTaskRemoved -= HandleMediaControlsServiceTaskRemoved; - - var serviceIntent = new Intent(Platform.AppContext, typeof(MediaControlsService)); - global::Android.App.Application.Context.StopService(serviceIntent); - Platform.AppContext.UnbindService(boundServiceConnection); - } - - void HandleMediaControlsServiceTaskRemoved(object? sender, EventArgs e) => Player?.Stop(); - - async Task SetPlayerData(CancellationToken cancellationToken = default) - { - if (MediaElement.Source is null) + if (!File.Exists(filePath)) { return null; } - - switch (MediaElement.Source) - { - case UriMediaSource uriMediaSource: - { - var uri = uriMediaSource.Uri; - if (!string.IsNullOrWhiteSpace(uri?.AbsoluteUri)) - { - return await CreateMediaItem(uri.AbsoluteUri, cancellationToken).ConfigureAwait(false); - } - - break; - } - case FileMediaSource fileMediaSource: - { - var filePath = fileMediaSource.Path; - if (!string.IsNullOrWhiteSpace(filePath)) - { - return await CreateMediaItem(filePath, cancellationToken).ConfigureAwait(false); - } - - break; - } - case ResourceMediaSource resourceMediaSource: - { - var package = PlayerView?.Context?.PackageName ?? ""; - var path = resourceMediaSource.Path; - if (!string.IsNullOrWhiteSpace(path)) - { - var assetFilePath = $"asset://{package}{Path.PathSeparator}{path}"; - return await CreateMediaItem(assetFilePath, cancellationToken).ConfigureAwait(false); - } - - break; - } - default: - throw new NotSupportedException($"{MediaElement.Source.GetType().FullName} is not yet supported for {nameof(MediaElement.Source)}"); - } - - return mediaItem; + using var stream = File.OpenRead(filePath); + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream, cancellationToken); + return memoryStream.ToArray(); } - async Task CreateMediaItem(string url, CancellationToken cancellationToken = default) + static async Task GetMauiAssetBytes(string? fileName, CancellationToken cancellationToken = default) { - 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) + if (fileName is null || string.IsNullOrEmpty(fileName)) { - mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover); + return null; } - - mediaItem = new MediaItem.Builder(); - mediaItem.SetUri(url); - mediaItem.SetMediaId(url); - mediaItem.SetMediaMetadata(mediaMetaData.Build()); - - return mediaItem; + fileName = System.IO.Path.GetFileName(fileName); + using Stream stream = await FileSystem.OpenAppPackageFileAsync(fileName); + using MemoryStream memoryStream = new(); + await stream.CopyToAsync(memoryStream, cancellationToken); + return memoryStream.ToArray(); } #region PlayerListener implementation method stubs @@ -766,23 +762,20 @@ public void OnTimelineChanged(Timeline? timeline, int reason) { } public void OnTrackSelectionParametersChanged(TrackSelectionParameters? trackSelectionParameters) { } public void OnTracksChanged(Tracks? tracks) { } #endregion - - 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; - } - - +} +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 de7a4be6b8..962d50ee85 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs @@ -223,49 +223,25 @@ protected virtual async partial ValueTask PlatformUpdateSource() metaData ??= new(Player); Metadata.ClearNowPlaying(); PlayerViewController?.ContentOverlayView?.Subviews.FirstOrDefault()?.RemoveFromSuperview(); - - if (MediaElement.Source is UriMediaSource uriMediaSource) + string? source; + bool isFileSource = false; + (source, isFileSource) = GetSource(MediaElement.Source); + if (!string.IsNullOrWhiteSpace(source)) { - var uri = uriMediaSource.Uri; - if (!string.IsNullOrWhiteSpace(uri?.AbsoluteUri)) + if (isFileSource) { - asset = AVAsset.FromUrl(new NSUrl(uri.AbsoluteUri)); - } - } - else if (MediaElement.Source is FileMediaSource fileMediaSource) - { - var uri = fileMediaSource.Path; - - if (!string.IsNullOrWhiteSpace(uri)) - { - asset = AVAsset.FromUrl(NSUrl.CreateFileUrl(uri)); - } - } - else if (MediaElement.Source is ResourceMediaSource resourceMediaSource) - { - var path = resourceMediaSource.Path; - - if (!string.IsNullOrWhiteSpace(path) && Path.HasExtension(path)) - { - string directory = Path.GetDirectoryName(path) ?? ""; - string filename = Path.GetFileNameWithoutExtension(path); - string extension = Path.GetExtension(path)[1..]; - var url = NSBundle.MainBundle.GetUrlForResource(filename, - extension, directory); - - asset = AVAsset.FromUrl(url); + asset = AVAsset.FromUrl(NSUrl.CreateFileUrl(source)); } else { - Logger.LogWarning("Invalid file path for ResourceMediaSource."); + asset = AVAsset.FromUrl(new NSUrl(source)); } } - PlayerItem = asset is not null ? new AVPlayerItem(asset) : null; - metaData.SetMetadata(PlayerItem, MediaElement); + await metaData.SetMetadata(PlayerItem, MediaElement); CurrentItemErrorObserver?.Dispose(); Player.ReplaceCurrentItemWithPlayerItem(PlayerItem); @@ -297,7 +273,6 @@ protected virtual async partial ValueTask PlatformUpdateSource() { Player.Play(); } - await SetPoster(); } else if (PlayerItem is null) @@ -460,6 +435,44 @@ protected virtual void Dispose(bool disposing) static TimeSpan ConvertTime(CMTime cmTime) => TimeSpan.FromSeconds(double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds); + static (string?, bool isFileSource) GetSource(MediaSource? source) + { + switch (source) + { + case UriMediaSource uriMediaSource: + var uri = uriMediaSource.Uri; + if (!string.IsNullOrWhiteSpace(uri?.AbsoluteUri)) + { + return (uri.AbsoluteUri, false); + } + break; + case FileMediaSource fileMediaSource: + var uriPath = fileMediaSource.Path; + if (!string.IsNullOrWhiteSpace(uriPath)) + { + return (uriPath, true); + } + break; + case ResourceMediaSource resourceMediaSource: + var path = resourceMediaSource.Path; + if (!string.IsNullOrWhiteSpace(path) && Path.HasExtension(path)) + { + string directory = Path.GetDirectoryName(path) ?? ""; + string filename = Path.GetFileNameWithoutExtension(path); + string extension = Path.GetExtension(path)[1..]; + var url = NSBundle.MainBundle.GetUrlForResource(filename, + extension, directory); + if (!string.IsNullOrEmpty(url?.AbsoluteString)) + { + return (url.AbsoluteString, false); + } + } + break; + case null: + return (null, false); + } + return (null, false); + } static async Task<(int Width, int Height)> GetVideoDimensions(AVPlayerItem avPlayerItem) { // Create an AVAsset instance with the video file URL @@ -498,9 +511,8 @@ protected virtual void Dispose(bool disposing) return asset.TracksWithMediaType(AVMediaTypes.Video.GetConstant() ?? "0").FirstOrDefault(); } - var tracks = await asset.LoadTracksWithMediaTypeAsync(AVMediaTypes.Video.GetConstant() ?? "0"); - - return tracks.Count <= 0 ? null : tracks[0]; + // On iOS 18+ and MacCatalyst 18+, use the new API or return null if not available + return null; } void AddStatusObservers() @@ -524,21 +536,57 @@ async Task SetPoster() return; } - var videoTrack = await GetTrack(PlayerItem.Asset); + AVAssetTrack? videoTrack = null; + if (PlayerItem.Asset is not null) + { +#if IOS || MACCATALYST + // Use the non-obsolete API for iOS 18+ and MacCatalyst 18+ + if (OperatingSystem.IsIOSVersionAtLeast(18) || OperatingSystem.IsMacCatalystVersionAtLeast(18)) + { + // On iOS 18+ and MacCatalyst 18+, AVAsset.TracksWithMediaType is obsolete. + // Instead, use the Tracks property and filter for video tracks. + videoTrack = PlayerItem.Asset.Tracks.FirstOrDefault(t => t.MediaType == AVMediaTypes.Video.GetConstant()); + } + else +#endif + { + // For earlier versions, use the existing API, but check for null + var videoMediaType = AVMediaTypes.Video.GetConstant(); + if (videoMediaType is not null) + { + videoTrack = PlayerItem.Asset.TracksWithMediaType(videoMediaType).FirstOrDefault(); + } + } + } if (videoTrack is not null) { return; } - if (PlayerItem.Asset.Tracks.Length == 0) + 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)) + if (PlayerViewController?.View is not null && PlayerViewController.ContentOverlayView is not null) { - var image = UIImage.LoadFromData(NSData.FromUrl(new NSUrl(MediaElement.MetadataArtworkUrl))) ?? new UIImage(); + string? source = null; + bool isFileSource = false; + (source, isFileSource) = GetSource(MediaElement.MetadataArtworkSource); + if (string.IsNullOrWhiteSpace(source)) + { + return; + } + UIImage? image = null; + if (isFileSource) + { + image = UIImage.FromFile(source) ?? new UIImage(); + } + else + { + image = UIImage.LoadFromData(NSData.FromUrl(new NSUrl(source))) ?? new UIImage(); + } var imageView = new UIImageView(image) { ContentMode = UIViewContentMode.ScaleAspectFit, @@ -554,7 +602,7 @@ async Task SetPoster() 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) ]); @@ -633,11 +681,10 @@ void StatusChanged(NSObservedChange obj) MediaElement.CurrentStateChanged(newState); } - - void TimeControlStatusChanged(NSObservedChange obj) + async void TimeControlStatusChanged(NSObservedChange obj) { if (Player is null || Player.Status is AVPlayerStatus.Unknown - || Player.CurrentItem?.Error is not null) + || Player.CurrentItem?.Error is not null || metaData is null) { return; } @@ -649,8 +696,7 @@ void TimeControlStatusChanged(NSObservedChange obj) AVPlayerTimeControlStatus.WaitingToPlayAtSpecifiedRate => MediaElementState.Buffering, _ => MediaElement.CurrentState }; - - metaData?.SetMetadata(PlayerItem, MediaElement); + await metaData.SetMetadata(PlayerItem, MediaElement); MediaElement.CurrentStateChanged(newState); } diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs index d12af56c84..37c277d5a8 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs @@ -1,27 +1,21 @@ -using System.Diagnostics; using System.Numerics; -using CommunityToolkit.Maui.Core.Primitives; using CommunityToolkit.Maui.Extensions; using CommunityToolkit.Maui.Views; using Microsoft.Extensions.Logging; -using Microsoft.Maui; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Dispatching; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Imaging; 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; -using WinMediaSource = Windows.Media.Core.MediaSource; namespace CommunityToolkit.Maui.Core.Views; partial class MediaManager : IDisposable { - Metadata? metadata; SystemMediaTransportControls? systemMediaControls; // States that allow changing position @@ -278,7 +272,7 @@ protected virtual async partial ValueTask PlatformUpdateSource() MediaElement.MediaWidth = MediaElement.MediaHeight = 0; MediaElement.CurrentStateChanged(MediaElementState.None); - + await UpdateMetadata(); return; } @@ -286,39 +280,69 @@ protected virtual async partial ValueTask PlatformUpdateSource() MediaElement.Duration = TimeSpan.Zero; Player.AutoPlay = MediaElement.ShouldAutoPlay; - if (MediaElement.Source is UriMediaSource uriMediaSource) + var source = GetSource(MediaElement.Source); + if (string.IsNullOrWhiteSpace(source)) + { + Logger.LogWarning("MediaElement Source is null or empty."); + return; + } + if (MediaElement.Source is UriMediaSource) + { + Player.MediaPlayer.SetUriSource(new Uri(source)); + } + else if (MediaElement.Source is FileMediaSource) + { + if (!File.Exists(source)) + { + Logger.LogWarning("FileMediaSource file not found: {FilePath}", source); + return; + } + StorageFile storageFile = await StorageFile.GetFileFromPathAsync(source); + Player.MediaPlayer.SetFileSource(storageFile); + } + else if (MediaElement.Source is ResourceMediaSource) + { + string path = GetFullAppPackageFilePath(source); + if (!string.IsNullOrWhiteSpace(path)) + { + Player.MediaPlayer.SetUriSource(new Uri(path)); + } + } + } + + string GetSource(MediaSource? source) + { + if (source == null) + { + return string.Empty; + } + if (source is UriMediaSource uriMediaSource) { var uri = uriMediaSource.Uri?.AbsoluteUri; if (!string.IsNullOrWhiteSpace(uri)) { - Player.MediaPlayer.SetUriSource(new Uri(uri)); + return uri; } } - else if (MediaElement.Source is FileMediaSource fileMediaSource) + else if (source is FileMediaSource fileMediaSource) { var filename = fileMediaSource.Path; if (!string.IsNullOrWhiteSpace(filename)) { - StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename); - Player.MediaPlayer.SetFileSource(storageFile); + return filename; } } - else if (MediaElement.Source is ResourceMediaSource resourceMediaSource) + else if (source is ResourceMediaSource resourceMediaSource) { if (string.IsNullOrWhiteSpace(resourceMediaSource.Path)) { Logger.LogInformation("ResourceMediaSource Path is null or empty"); - return; - } - - string path = GetFullAppPackageFilePath(resourceMediaSource.Path); - if (!string.IsNullOrWhiteSpace(path)) - { - Player.MediaPlayer.SetUriSource(new Uri(path)); + return string.Empty; } + return resourceMediaSource.Path; } + return string.Empty; } - protected virtual partial void PlatformUpdateShouldLoopPlayback() { if (Player is null) @@ -384,33 +408,70 @@ async ValueTask UpdateMetadata() return; } - metadata ??= new(systemMediaControls, MediaElement, Dispatcher); - metadata.SetMetadata(MediaElement); - if (string.IsNullOrEmpty(MediaElement.MetadataArtworkUrl)) + var source = GetSource(MediaElement.MetadataArtworkSource); + + RandomAccessStreamReference? stream = null; + StorageFile? file = null; + Uri? uri = null; + switch (MediaElement.MetadataArtworkSource) { - return; + case UriMediaSource: + if (!string.IsNullOrWhiteSpace(source) && Uri.TryCreate(source, UriKind.Absolute, out var artworkUri)) + { + stream = RandomAccessStreamReference.CreateFromUri(artworkUri); + uri = artworkUri; + } + else + { + Logger.LogWarning("UriMediaSource metadata artwork source is null, empty, or invalid."); + } + break; + case FileMediaSource: + if (File.Exists(source)) + { + file = await StorageFile.GetFileFromPathAsync(source); + stream = RandomAccessStreamReference.CreateFromFile(file); + uri = new(source); + } + break; + case ResourceMediaSource: + try + { + if (string.IsNullOrEmpty(source)) + { + Logger.LogWarning("ResourceMediaSource metadata artwork source path is null or empty."); + return; + } + string path = GetFullAppPackageFilePath(source); + file = await StorageFile.GetFileFromPathAsync(path); + stream = RandomAccessStreamReference.CreateFromFile(file); + uri = new(file.Path); + } + catch (FileNotFoundException e) + { + Logger.LogWarning("ResourceMediaSource file not found: {Message}", e.Message); + } + break; } - if (!Uri.TryCreate(MediaElement.MetadataArtworkUrl, UriKind.RelativeOrAbsolute, out var metadataArtworkUri)) + + if (source is not null && stream is not null && uri is not null) { - Trace.TraceError($"{nameof(MediaElement)} unable to update artwork because {nameof(MediaElement.MetadataArtworkUrl)} is not a valid URI"); - return; + systemMediaControls.DisplayUpdater.Thumbnail = stream; + Dispatcher.Dispatch(() => Player.PosterSource = new BitmapImage(uri)); } - if (Dispatcher.IsDispatchRequired) - { - await Dispatcher.DispatchAsync(() => UpdatePosterSource(Player, metadataArtworkUri)); - } else { - UpdatePosterSource(Player, metadataArtworkUri); + systemMediaControls.DisplayUpdater.Thumbnail = null; + Dispatcher.Dispatch(() => Player.PosterSource = new BitmapImage()); } - static void UpdatePosterSource(in MediaPlayerElement player, in Uri metadataArtworkUri) - { - player.PosterSource = new BitmapImage(metadataArtworkUri); - } + systemMediaControls.DisplayUpdater.Type = MediaPlaybackType.Music; + systemMediaControls.DisplayUpdater.MusicProperties.Artist = MediaElement.MetadataArtist; + systemMediaControls.DisplayUpdater.MusicProperties.Title = MediaElement.MetadataTitle; + systemMediaControls.DisplayUpdater.Update(); } - + async void OnMediaElementMediaOpened(WindowsMediaElement sender, object args) { if (Player is null) @@ -525,7 +586,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 93b6f2e1ff..89af82f43a 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs @@ -17,7 +17,7 @@ public void VerifyDefaults() { // Arrange MediaElement mediaElement = new(); - + // Act // Assert @@ -27,7 +27,7 @@ public void VerifyDefaults() Assert.Equal(MediaElementDefaults.Duration, mediaElement.Duration); Assert.Equal(MediaElementDefaults.MediaWidth, mediaElement.MediaWidth); Assert.Equal(MediaElementDefaults.MetadataArtist, mediaElement.MetadataArtist); - Assert.Equal(MediaElementDefaults.MetadataArtworkUrl, mediaElement.MetadataArtworkUrl); + Assert.Equal(MediaElementDefaults.MetadataArtworkSource, mediaElement.MetadataArtworkSource); Assert.Equal(MediaElementDefaults.Position, mediaElement.Position); Assert.Equal(MediaElementDefaults.ShouldAutoPlay, mediaElement.ShouldAutoPlay); Assert.Equal(MediaElementDefaults.ShouldKeepScreenOn, mediaElement.ShouldKeepScreenOn); @@ -38,22 +38,22 @@ public void VerifyDefaults() Assert.Equal(MediaElementDefaults.Volume, mediaElement.Volume); Assert.Equal(MediaElementDefaults.MetadataTitle, mediaElement.MetadataTitle); } - + [Fact] 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]