Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b631602
Refactor Android MediaElement to use MediaSession
ne0rrmatrix Oct 2, 2025
ea992b3
Refactor getting artwork to use native methods. Fixes issue with navi…
ne0rrmatrix Oct 2, 2025
3dfb124
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 2, 2025
dc0cc91
Refactor media playback and UI improvements
ne0rrmatrix Oct 2, 2025
c0c7eb8
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Oct 2, 2025
efdde5b
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Oct 2, 2025
336d5ab
Add CancellationToken support to CreateMediaController
ne0rrmatrix Oct 2, 2025
33da943
Fix text selection and add dash sample to sample app
ne0rrmatrix Oct 3, 2025
a712311
Fix layout issue
ne0rrmatrix Oct 3, 2025
d1f2fc3
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 8, 2025
83cac6c
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 10, 2025
d33a686
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 15, 2025
e4fcf8e
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 16, 2025
b95adbb
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 25, 2025
e779753
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 27, 2025
1acf0f0
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Oct 30, 2025
d7dd2ef
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Nov 4, 2025
5dff776
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Nov 5, 2025
e33100a
Fix merge
ne0rrmatrix Nov 20, 2025
fd4e473
Update samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/…
ne0rrmatrix Nov 20, 2025
7971fd3
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
7e286ac
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
3603e79
Update src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHa…
ne0rrmatrix Nov 20, 2025
bc3d944
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
55b6062
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
edfed8e
Refactor media services and improve resource handling
ne0rrmatrix Nov 20, 2025
ca87c60
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
7ca828b
Update src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHa…
ne0rrmatrix Nov 20, 2025
8a27ddc
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 20, 2025
8d4139f
Fix async void error
ne0rrmatrix Nov 20, 2025
4a2dd06
Fix dispose bug
ne0rrmatrix Nov 20, 2025
9155b08
Refactor MediaManager for cleanup and resource handling
ne0rrmatrix Nov 20, 2025
e777736
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Nov 21, 2025
cbb31ad
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Nov 23, 2025
cfc48ec
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Nov 23, 2025
b7084c7
Apply suggestion from @Copilot
ne0rrmatrix Nov 23, 2025
2a96736
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Nov 24, 2025
3ac2469
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Dec 4, 2025
5e2aac8
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Dec 9, 2025
badbcc8
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Dec 17, 2025
373086b
Multiple media players with associated sessions now supported
ne0rrmatrix Dec 18, 2025
02c7e49
Merge branch 'AndroidServiceUpdate' of https://github.com/ne0rrmatrix…
ne0rrmatrix Dec 18, 2025
7fd4161
Remove function call that was not relevant
ne0rrmatrix Dec 18, 2025
8c5677b
Refactor to simplify code
ne0rrmatrix Dec 18, 2025
2131c78
Fix feature accidently removed
ne0rrmatrix Dec 18, 2025
93e22f8
Refactor to reduce code
ne0rrmatrix Dec 18, 2025
29d1290
Fix double dispose
ne0rrmatrix Dec 18, 2025
df44b04
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Dec 22, 2025
0689336
Revert changes to add support for multiple Media Elements
ne0rrmatrix Dec 24, 2025
aead30e
Merge branch 'main' into AndroidServiceUpdate
ne0rrmatrix Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ public partial class MediaElementPage : BasePage<MediaElementViewModel>
{
const string loadOnlineMp4 = "Load Online MP4";
const string loadHls = "Load HTTP Live Stream (HLS)";
const string loadDASH = "Load MPEG-DASH (not supported on iOS/MacCatalyst)";
const string loadLocalResource = "Load Local Resource";
const string resetSource = "Reset Source to null";
const string loadMusic = "Load Music";

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 dashTestUrl = "https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd";
const string hal9000AudioUrl = "https://github.com/prof3ssorSt3v3/media-sample-files/raw/master/hal-9000.mp3";


readonly ILogger logger;
readonly IDeviceInfo deviceInfo;
readonly IFileSystem fileSystem;
Expand Down Expand Up @@ -166,7 +167,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, loadDASH, loadLocalResource, resetSource, loadMusic);

MediaElement.Stop();
MediaElement.Source = null;
Expand All @@ -188,6 +189,12 @@ async void ChangeSourceClicked(object? sender, EventArgs? e)
MediaElement.Source = MediaSource.FromUri(hlsStreamTestUrl);
return;

case loadDASH:
MediaElement.MetadataArtist = "DASH Album";
MediaElement.MetadataArtworkUrl = botImageUrl;
MediaElement.MetadataTitle = "DASH Title";
MediaElement.Source = MediaSource.FromUri(dashTestUrl);
return;
case resetSource:
MediaElement.MetadataArtworkUrl = string.Empty;
MediaElement.MetadataTitle = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(this MauiAppBui
h.AddHandler<MediaElement, MediaElementHandler>();
});

#if ANDROID
builder.Services.AddSingleton<Media.Services.MediaControlsService>();
#endif

return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,24 @@ protected override MauiMediaElement CreatePlatformView()
VirtualView,
Dispatcher.GetForCurrentThread() ?? throw new InvalidOperationException($"{nameof(IDispatcher)} cannot be null"));

var (_, playerView) = MediaManager.CreatePlatformView(VirtualView.AndroidViewType);
var playerView = MediaManager.CreatePlatformView(VirtualView.AndroidViewType);
return new(Context, playerView);
}

protected override async void ConnectHandler(MauiMediaElement platformView)
{
base.ConnectHandler(platformView);
if (platformView is null)
{
throw new InvalidOperationException($"{nameof(platformView)} cannot be null");
}
if (MediaManager is null)
{
throw new InvalidOperationException($"{nameof(MediaManager)} cannot be null");
}
var mediaController = await MediaManager.CreateMediaController();
platformView.SetView(mediaController);
await MediaManager.UpdateSource();
}
protected override void DisconnectHandler(MauiMediaElement platformView)
{
platformView.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/texture_view_media_element"
app:surface_type="texture_view"
app:shutter_background_color="#FFFFFF"
app:shutter_background_color="#000000"
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shutter background color has been changed from white (#FFFFFF) to black (#000000). While this may be intentional for consistency with the black theme used throughout the UI, ensure this change is documented and doesn't negatively impact the user experience, especially during loading states.

Suggested change
app:shutter_background_color="#000000"
app:shutter_background_color="#FFFFFF"

Copilot uses AI. Check for mistakes.
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:background="@android:color/transparent"
android:background="@android:color/black"
/>

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,180 +1,94 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using AndroidX.Core.App;
using AndroidX.Media3.Common;
using AndroidX.Media3.DataSource;
using AndroidX.Media3.ExoPlayer;
using AndroidX.Media3.ExoPlayer.TrackSelection;
using AndroidX.Media3.Session;
using AndroidX.Media3.UI;
using CommunityToolkit.Maui.Services;
using Resource = Microsoft.Maui.Controls.Resource;
using Java.Util;

namespace CommunityToolkit.Maui.Media.Services;

[SupportedOSPlatform("Android26.0")]
[IntentFilter(["androidx.media3.session.MediaSessionService"])]
[Service(Exported = false, Enabled = true, Name = "communityToolkit.maui.media.services", ForegroundServiceType = ForegroundService.TypeMediaPlayback)]
sealed partial class MediaControlsService : Service
sealed partial class MediaControlsService : MediaSessionService
{
readonly WeakEventManager taskRemovedEventManager = new();

bool isDisposed;

PlayerNotificationManager? playerNotificationManager;
NotificationCompat.Builder? notificationBuilder;

public event EventHandler TaskRemoved
{
add => taskRemovedEventManager.AddEventHandler(value);
remove => taskRemovedEventManager.RemoveEventHandler(value);
}

public BoundServiceBinder? Binder { get; private set; }
public NotificationManager? NotificationManager { get; private set; }

public override IBinder? OnBind(Intent? intent)
{
Binder = new BoundServiceBinder(this);
return Binder;
}

public override void OnCreate()
{
base.OnCreate();
StartForegroundServices();
}

public override StartCommandResult OnStartCommand(Intent? intent, StartCommandFlags flags, int startId)
=> StartCommandResult.NotSticky;
MediaSession? mediaSession;
IExoPlayer? exoPlayer;
DefaultTrackSelector? trackSelector;

public override void OnTaskRemoved(Intent? rootIntent)
{
base.OnTaskRemoved(rootIntent);
taskRemovedEventManager.HandleEvent(this, EventArgs.Empty, nameof(TaskRemoved));

playerNotificationManager?.SetPlayer(null);
NotificationManager?.CancelAll();
}

public override void OnDestroy()
{
base.OnDestroy();

playerNotificationManager?.SetPlayer(null);
NotificationManager?.CancelAll();
if (!OperatingSystem.IsAndroidVersionAtLeast(33))
{
StopForeground(true);
}

StopSelf();
}

public override void OnRebind(Intent? intent)
{
base.OnRebind(intent);
StartForegroundServices();
PauseAllPlayersAndStopSelf();
}

[MemberNotNull(nameof(NotificationManager), nameof(notificationBuilder))]
public void UpdateNotifications(in MediaSession session, in PlatformMediaElement mediaElement)
{
ArgumentNullException.ThrowIfNull(notificationBuilder);
ArgumentNullException.ThrowIfNull(NotificationManager);

var style = new MediaStyleNotificationHelper.MediaStyle(session);
if (!OperatingSystem.IsAndroidVersionAtLeast(33))
{
SetLegacyNotifications(session, mediaElement);
}

notificationBuilder.SetStyle(style);
NotificationManagerCompat.From(Platform.AppContext)?.Notify(1, notificationBuilder.Build());
}

[MemberNotNull(nameof(playerNotificationManager))]
public void SetLegacyNotifications(in MediaSession session, in PlatformMediaElement mediaElement)
public override void OnCreate()
{
ArgumentNullException.ThrowIfNull(session);
playerNotificationManager ??= new PlayerNotificationManager.Builder(Platform.AppContext, 1, "1").Build()
?? throw new InvalidOperationException("PlayerNotificationManager cannot be null");
base.OnCreate();

playerNotificationManager.SetUseFastForwardAction(true);
playerNotificationManager.SetUseFastForwardActionInCompactView(true);
playerNotificationManager.SetUseRewindAction(true);
playerNotificationManager.SetUseRewindActionInCompactView(true);
playerNotificationManager.SetUseNextAction(true);
playerNotificationManager.SetUseNextActionInCompactView(true);
playerNotificationManager.SetUsePlayPauseActions(true);
playerNotificationManager.SetUsePreviousAction(true);
playerNotificationManager.SetColor(Resource.Color.abc_primary_text_material_dark);
playerNotificationManager.SetUsePreviousActionInCompactView(true);
playerNotificationManager.SetVisibility(NotificationCompat.VisibilityPublic);
playerNotificationManager.SetMediaSessionToken(session.PlatformToken);
playerNotificationManager.SetPlayer(mediaElement);
playerNotificationManager.SetColorized(true);
playerNotificationManager.SetShowPlayButtonIfPlaybackIsSuppressed(true);
playerNotificationManager.SetSmallIcon(Resource.Drawable.media3_notification_small_icon);
playerNotificationManager.SetPriority(NotificationCompat.PriorityDefault);
playerNotificationManager.SetUseChronometer(true);
var audioAttribute = new AndroidX.Media3.Common.AudioAttributes.Builder()?
.SetContentType(C.AudioContentTypeMusic)? // When phonecalls come in, music is paused
.SetUsage(C.UsageMedia)?
.Build();

trackSelector = new DefaultTrackSelector(this);
var trackSelectionParameters = trackSelector.BuildUponParameters()?
.SetPreferredAudioLanguage(C.LanguageUndetermined)? // Fallback to system language if no preferred language found
.SetPreferredTextLanguage(C.LanguageUndetermined)? // Fallback to system language if no preferred language found
.SetIgnoredTextSelectionFlags(C.SelectionFlagAutoselect); // Ignore text tracks that are not explicitly selected by the user
trackSelector.SetParameters((DefaultTrackSelector.Parameters.Builder?)trackSelectionParameters); // Allows us to select tracks based on user preferences

var loadControlBuilder = new DefaultLoadControl.Builder();
loadControlBuilder.SetBufferDurationsMs(
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000); // Custom buffering strategy

var builder = new ExoPlayerBuilder(this) ?? throw new InvalidOperationException("ExoPlayerBuilder returned null");
builder.SetTrackSelector(trackSelector);
builder.SetAudioAttributes(audioAttribute, true);
builder.SetHandleAudioBecomingNoisy(true); // Unplugging headphones will pause playback
builder.SetLoadControl(loadControlBuilder.Build());
exoPlayer = builder.Build() ?? throw new InvalidOperationException("ExoPlayerBuilder.Build() returned null");

var mediaSessionBuilder = new MediaSession.Builder(this, exoPlayer);
UUID sessionId = UUID.RandomUUID() ?? throw new InvalidOperationException("UUID.RandomUUID() returned null");
mediaSessionBuilder.SetId(sessionId.ToString());

var dataSourceBitmapFactory = new DataSourceBitmapLoader(this);
mediaSessionBuilder.SetBitmapLoader(dataSourceBitmapFactory);
mediaSession = mediaSessionBuilder.Build() ?? throw new InvalidOperationException("MediaSession.Builder.Build() returned null");
}

protected override void Dispose(bool disposing)
{
if (!isDisposed)
if (disposing)
{
if (disposing)
{
NotificationManager?.Dispose();
NotificationManager = null;

playerNotificationManager?.Dispose();
playerNotificationManager = null;

if (!OperatingSystem.IsAndroidVersionAtLeast(33))
{
StopForeground(true);
}

StopSelf();
}

isDisposed = true;
PauseAllPlayersAndStopSelf();
mediaSession?.Release();
mediaSession?.Dispose();
mediaSession = null;
exoPlayer?.Release();
Comment on lines +75 to +76
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exoPlayer is disposed in the Dispose method (line 77) but is not stopped or cleared before disposal. According to best practices and the pattern used in MediaManager.android.cs (lines 564-567), you should call exoPlayer.Stop() and exoPlayer.ClearMediaItems() before calling Release() and Dispose() to ensure proper cleanup.

Suggested change
mediaSession = null;
exoPlayer?.Release();
mediaSession = null;
exoPlayer?.Stop();
exoPlayer?.ClearMediaItems();
exoPlayer?.Release();
exoPlayer?.Dispose();

Copilot uses AI. Check for mistakes.
exoPlayer = null;
trackSelector?.Dispose();
trackSelector = null;
}

base.Dispose(disposing);
}

static void CreateNotificationChannel(in NotificationManager notificationMnaManager)
public override void OnDestroy()
{
var channel = new NotificationChannel("1", "1", NotificationImportance.Low);
notificationMnaManager.CreateNotificationChannel(channel);
base.OnDestroy();
PauseAllPlayersAndStopSelf();
}

[MemberNotNull(nameof(notificationBuilder), nameof(NotificationManager))]
void StartForegroundServices()
public override MediaSession? OnGetSession(MediaSession.ControllerInfo? p0)
{
NotificationManager ??= GetSystemService(NotificationService) as NotificationManager ?? throw new InvalidOperationException($"{nameof(NotificationManager)} cannot be null");
notificationBuilder ??= new NotificationCompat.Builder(Platform.AppContext, "1");

notificationBuilder.SetSmallIcon(Resource.Drawable.media3_notification_small_icon);
notificationBuilder.SetAutoCancel(false);
notificationBuilder.SetForegroundServiceBehavior(NotificationCompat.ForegroundServiceImmediate);
notificationBuilder.SetVisibility(NotificationCompat.VisibilityPublic);

CreateNotificationChannel(NotificationManager);

if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
if (notificationBuilder.Build() is Notification notification)
{
StartForeground(1, notification, ForegroundService.TypeMediaPlayback);
}
}
else
{
StartForeground(1, notificationBuilder.Build());
}
return mediaSession;
}
}
Loading
Loading