-
Notifications
You must be signed in to change notification settings - Fork 475
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
base: main
Are you sure you want to change the base?
Conversation
Updated `PlatformUpdateSource` to be asynchronous, re-implemented `SetPoster` method for robustness, and replaced `MetadataArtworkUrl` with `MetadataArtworkSource` across the project. - Replaced `MetadataArtworkUrl` with `MetadataArtworkSource` in `MediaElementPage.xaml` and related files. - Introduced `loadCustomMediaSource` constant and `saveDirectory` string in `MediaElementPage.xaml.cs`. - Updated `ChangeSourceClicked` method to handle new media source and artwork property. - Added file handling methods: `Savefile`, `GetFileName`, and `PickAndShow`. - Updated `IMediaElement` and `MediaElement` class to use `MetadataArtworkSource`. - Modified `SetMetadata` in `Metadata.macios.cs` for new artwork property. - Removed `Metadata` class from `Metadata.windows.cs`. - Enhanced `MediaManager.android.cs` to handle new artwork property and fetch image data. - Added `BlankByteArray` method and `PlaybackState` class in `MediaManager.android.cs`. - Made `PlatformUpdateSource` in `MediaManager.macios.cs` asynchronous, updated `Dispose` method. - Added `GetArtwork` struct for fetching artwork in `MediaManager.macios.cs`. - Updated `MediaManager.windows.cs` to handle new artwork property and added `ArtworkUrl` method. - Updated `MediaElementTests` for new artwork property.
Simplified `SetMetadata` in `Metadata.macios.cs` by removing a null check for `artwork` before checking if it is a `UIImage`. Cleaned up `Dispose` in `MediaManager.android.cs` by removing redundant empty lines. Streamlined `StopService` in `MediaManager.android.cs` by removing `HttpClient` usage and `GetBytesFromMetadataArtworkUrl` method. Updated `Dispose` in `MediaManager.macios.cs` to change `SetPoster` return type from `Task` to `ValueTask`. Refactored `UpdateMetadata` in `MediaManager.windows.cs` to handle different `MetadataArtworkSource` types explicitly and removed redundant `ArtworkUrl` method. Cleaned up `OnPlaybackSessionPlaybackStateChanged` in `MediaManager.windows.cs` by removing the now redundant `ArtworkUrl` method.
… into MetaDataSource
69936ec to
9347a3a
Compare
… into MetaDataSource
Replaces MetadataArtworkUrl (string) with MetadataArtworkSource (MediaSource) for greater flexibility in specifying artwork (URI, file, or resource). Updates interfaces, platform code, and tests to use the new property. Removes obsolete code and adds helper methods for extracting artwork sources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds support for local files and package resources as sources for MediaElement.MetadataArtworkSource, extending beyond the previous URL-only support. The property type has been changed from string (URL) to MediaSource to enable different source types (URI, File, and Resource).
Changes:
- Changed
MetadataArtworkUrlproperty fromstringtoMetadataArtworkSourceof typeMediaSourceto support URI, File, and Resource media sources - Implemented platform-specific handlers for loading artwork from different source types on Android, iOS/MacCatalyst, and Windows
- Updated tests and samples to use the new
MetadataArtworkSourceproperty
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs | Changed property from MetadataArtworkUrl (string) to MetadataArtworkSource (MediaSource) with TypeConverter |
| src/CommunityToolkit.Maui.MediaElement/Interfaces/IMediaElement.shared.cs | Updated interface to reflect MetadataArtworkSource property change |
| src/CommunityToolkit.Maui.MediaElement/Primitives/MediaElementDefaults.shared.cs | Changed default from string to MediaSource type |
| src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs | Added methods to load artwork from URI, File, and Resource sources on Android |
| src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs | Refactored to use GetSource helper and updated poster loading for different source types |
| src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.windows.cs | Implemented UpdateMetadata to handle different artwork source types and added GetSource helper |
| src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.macios.cs | Added methods to load artwork images from different source types for macOS/iOS |
| src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs | Updated SetMetadata signature (appears to be unused/dead code) |
| src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementTests.cs | Updated tests to use MediaSource type instead of string |
| samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml | Updated XAML to use MetadataArtworkSource property |
| samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs | Updated code-behind to use MetadataArtworkSource property |
Comments suppressed due to low confidence (1)
src/CommunityToolkit.Maui.MediaElement/Primitives/Metadata.windows.cs:75
- Dead code: The Metadata class in this file appears to no longer be instantiated or used in the Windows implementation. The metadata handling has been moved directly into MediaManager.windows.cs UpdateMetadata method. This file should be removed or the changes to SetMetadata should be reverted if it's not being used.
using CommunityToolkit.Maui.Core.Views;
using Microsoft.Maui.Dispatching;
using Windows.Media;
namespace CommunityToolkit.Maui.Core.Primitives;
sealed class Metadata
{
readonly IMediaElement? mediaElement;
readonly SystemMediaTransportControls? systemMediaControls;
readonly IDispatcher dispatcher;
/// <summary>
/// Initializes a new instance of the <see cref="Metadata"/> class.
/// </summary>
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();
}
}
}
/// <summary>
/// Sets the metadata for the given MediaElement.
/// </summary>
public void SetMetadata(IMediaElement mp, MediaManager mediaManager)
{
if (systemMediaControls is null || mediaElement is null || mp is null)
{
return;
}
var source = mediaManager.GetSource(mp.MetadataArtworkSource);
if (!string.IsNullOrEmpty(source))
{
systemMediaControls.DisplayUpdater.Thumbnail = Windows.Storage.Streams.RandomAccessStreamReference.CreateFromUri(new Uri(source));
}
systemMediaControls.DisplayUpdater.Type = MediaPlaybackType.Music;
systemMediaControls.DisplayUpdater.MusicProperties.Artist = mp.MetadataTitle;
systemMediaControls.DisplayUpdater.MusicProperties.Title = mp.MetadataArtist;
systemMediaControls.DisplayUpdater.Update();
}
}
| 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)); | ||
| } | ||
| } |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maintainability issue: The if statements checking different MediaSource types on lines 421, 434, and 449 should use else-if instead of separate if statements. Once one type matches, the others won't, so checking them all separately is inefficient and could lead to unexpected behavior if the conditions were ever modified.
| [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<MediaSource>(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); | ||
| } |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage issue: The tests only verify setting MetadataArtworkSource to a string (URI) and null. There is no test coverage for FileMediaSource and ResourceMediaSource types, which are the main additions in this PR. Tests should be added to verify that MetadataArtworkSource can be set to FileMediaSource.FromFile() and MediaSource.FromResource().
| StorageFile ImageFile = await StorageFile.GetFileFromPathAsync(artwork); | ||
| Dispatcher.Dispatch(async () => | ||
| { | ||
| var bitmap = await LoadBitmapImageAsync(ImageFile); | ||
| Player.PosterSource = bitmap; | ||
| }); | ||
| systemMediaControls.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromFile(ImageFile); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming issue: Variable naming is inconsistent with C# conventions. The variable name should be imageFile (camelCase) instead of ImageFile (PascalCase). PascalCase should only be used for types, methods, and properties, not local variables.
| StorageFile ImageFile = await StorageFile.GetFileFromPathAsync(artwork); | |
| Dispatcher.Dispatch(async () => | |
| { | |
| var bitmap = await LoadBitmapImageAsync(ImageFile); | |
| Player.PosterSource = bitmap; | |
| }); | |
| systemMediaControls.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromFile(ImageFile); | |
| StorageFile imageFile = await StorageFile.GetFileFromPathAsync(artwork); | |
| Dispatcher.Dispatch(async () => | |
| { | |
| var bitmap = await LoadBitmapImageAsync(imageFile); | |
| Player.PosterSource = bitmap; | |
| }); | |
| systemMediaControls.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromFile(imageFile); |
| NSData? nsdata = NSData.FromStream(memoryStream); | ||
| if (nsdata is null) | ||
| { | ||
| System.Diagnostics.Trace.TraceInformation($"{nsdata} is null."); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error message issue: The error message uses string interpolation with the variable name nsdata which will output the type name, not a meaningful message. The message should be something like "Failed to create NSData from stream for resource '{resource}'." or simply "Failed to load image data from stream."
| System.Diagnostics.Trace.TraceInformation($"{nsdata} is null."); | |
| System.Diagnostics.Trace.TraceInformation($"Failed to create NSData from stream for resource '{resource}'."); |
| 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)); | ||
| } |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference: When artwork is an empty string (line 423), a new Uri is still created from it on line 429, which could throw a UriFormatException. The check on line 425 only verifies that file is not null, which doesn't guarantee that artwork is a valid URI string. Add a validation to ensure artwork is not empty before creating the URI.
| 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 (uriMediaSource.Uri is null) | |
| { | |
| // No valid artwork URI available; skip updating artwork from UriMediaSource | |
| } | |
| else | |
| { | |
| var artworkUri = uriMediaSource.Uri; | |
| var file = RandomAccessStreamReference.CreateFromUri(artworkUri); | |
| if (file is not null) | |
| { | |
| systemMediaControls.DisplayUpdater.Thumbnail = file; | |
| systemMediaControls.DisplayUpdater.Update(); | |
| Dispatcher.Dispatch(() => Player.PosterSource = new BitmapImage(artworkUri)); | |
| } | |
| } |
| 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"); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spelling error: "Arkwork" should be "Artwork" in multiple trace messages throughout this method. This typo appears on lines 763, 773, and 784.
| var stream = File.OpenRead(filePath); | ||
| var memoryStream = new MemoryStream(); | ||
| await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); | ||
| var bytes = memoryStream.ToArray(); | ||
| return bytes; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resource leak: The streams opened in this method are not disposed properly. Both stream and memoryStream should be wrapped in using statements or disposed explicitly to prevent resource leaks.
| 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(); |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resource leak: The MemoryStream created is not disposed properly. It should be wrapped in a using statement to prevent resource leaks.
| async ValueTask UpdateMetadata() | ||
| { | ||
| if (systemMediaControls is null || Player is null) | ||
| if (systemMediaControls is null || Player is null || MediaElement.MetadataArtworkSource is null) |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The UpdateMetadata method returns early if MetadataArtworkSource is null (line 416), but then the code at lines 462-465 will never execute when the artwork source is null. This means metadata like Title and Artist won't be set when there's no artwork. The null check for MetadataArtworkSource should be removed from line 416, and null checks should be added within each if block that handles the artwork source.
| if (systemMediaControls is null || Player is null || MediaElement.MetadataArtworkSource is null) | |
| if (systemMediaControls is null || Player is null) |
| if (artwork is UIImage image) | ||
| { | ||
| NowPlayingInfo.Artwork = new(boundsSize: new(320, 240), requestHandler: _ => image); | ||
| } | ||
| else | ||
| { | ||
| NowPlayingInfo.Artwork = new(boundsSize: new(0, 0), requestHandler: _ => defaultUIImage); | ||
| } |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
[Proposal] Add support for local files and package resources to MediaElement #2151
Description of Change
Add support for file and resource file types to
MediaElement.MetadataArtworkSource. This will allow developers to set artwork usingMediaSourceclass.Linked Issues
PR Checklist
approved(bug) orChampioned(feature/proposal)mainat time of PRAdditional information
Summary
Add support for local files and package resources as a source for
MediaElement.MetadataArtworkUrl. This will add the missing support for all types of files on all device for artwork images.Motivation
Allow developer more options to add images from more locations to use as artwork for player.
Detailed Design
API Design:
Usage Syntax
XAML:
Code Behind: