Skip to content

Conversation

@ne0rrmatrix
Copy link
Member

Description of Change

Add support for file and resource file types to MediaElement.MetadataArtworkSource. This will allow developers to set artwork using MediaSource class.

Linked Issues

PR Checklist

Additional 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:

/// <summary>
	/// Backing store for the <see cref="MetadataArtworkUrl"/> property.
	/// </summary>
	public static readonly BindableProperty MetadataArtworkUrlProperty = BindableProperty.Create(nameof(MetadataArtworkSource), typeof(MediaSource), typeof(MediaElement));
/// Gets or sets the Artwork Image Url of the media.
	/// This is a bindable property.
	/// </summary>
	[TypeConverter(typeof(MediaSourceConverter))]
	public MetadataArtworkSource? MetadataArtworkSource
	{
		get => (MediaSource)GetValue(MetadataArtworkUrlProperty);
		set => SetValue(MetadataArtworkUrlProperty, value);
	}

Usage Syntax

XAML:

 <toolkit:MediaElement
     x:Name="MediaElement"
     ShouldAutoPlay="True"
     Source="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
     MetadataArtworkSource="ebedded://robot.jpg"
     MetadataTitle="Big Buck Bunny"
     MetadataArtist="Blender Foundation"/>

Code Behind:

MediaElement.MetadataArtworkSource = MediaSource.FromResource("robot.jpg");  

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.
@dotnet-policy-service dotnet-policy-service bot added stale The author has not responded in over 30 days help wanted This proposal has been approved and is ready to be implemented labels May 1, 2025
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.
Copilot AI review requested due to automatic review settings January 29, 2026 06:13
Copy link
Contributor

Copilot AI left a 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 MetadataArtworkUrl property from string to MetadataArtworkSource of type MediaSource to 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 MetadataArtworkSource property

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();
	}
}

Comment on lines +421 to 460
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));
}
}
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 57
[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);
}
Copy link

Copilot AI Jan 29, 2026

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().

Copilot uses AI. Check for mistakes.
Comment on lines +439 to +445
StorageFile ImageFile = await StorageFile.GetFileFromPathAsync(artwork);
Dispatcher.Dispatch(async () =>
{
var bitmap = await LoadBitmapImageAsync(ImageFile);
Player.PosterSource = bitmap;
});
systemMediaControls.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromFile(ImageFile);
Copy link

Copilot AI Jan 29, 2026

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
NSData? nsdata = NSData.FromStream(memoryStream);
if (nsdata is null)
{
System.Diagnostics.Trace.TraceInformation($"{nsdata} is null.");
Copy link

Copilot AI Jan 29, 2026

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."

Suggested change
System.Diagnostics.Trace.TraceInformation($"{nsdata} is null.");
System.Diagnostics.Trace.TraceInformation($"Failed to create NSData from stream for resource '{resource}'.");

Copilot uses AI. Check for mistakes.
Comment on lines +423 to +431
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));
}
Copy link

Copilot AI Jan 29, 2026

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.

Suggested change
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));
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +763 to +784
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");
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +813 to +817
var stream = File.OpenRead(filePath);
var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
var bytes = memoryStream.ToArray();
return bytes;
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +829 to +833
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();
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
async ValueTask UpdateMetadata()
{
if (systemMediaControls is null || Player is null)
if (systemMediaControls is null || Player is null || MediaElement.MetadataArtworkSource is null)
Copy link

Copilot AI Jan 29, 2026

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.

Suggested change
if (systemMediaControls is null || Player is null || MediaElement.MetadataArtworkSource is null)
if (systemMediaControls is null || Player is null)

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +89
if (artwork is UIImage image)
{
NowPlayingInfo.Artwork = new(boundsSize: new(320, 240), requestHandler: _ => image);
}
else
{
NowPlayingInfo.Artwork = new(boundsSize: new(0, 0), requestHandler: _ => defaultUIImage);
}
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted This proposal has been approved and is ready to be implemented stale The author has not responded in over 30 days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant