Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
39 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
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 @@ -32,10 +32,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"
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,117 @@
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 Java.Util;
using Resource = Microsoft.Maui.Controls.Resource;

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();
const string cHANNEL_ID = "media_playback_channel";
const int nOTIFICATION_ID = 1001;

bool isDisposed;
MediaSession? mediaSession;
public IExoPlayer? ExoPlayer;

PlayerNotificationManager? playerNotificationManager;
NotificationCompat.Builder? notificationBuilder;

public event EventHandler TaskRemoved
public override void OnTaskRemoved(Intent? rootIntent)
{
add => taskRemovedEventManager.AddEventHandler(value);
remove => taskRemovedEventManager.RemoveEventHandler(value);
base.OnTaskRemoved(rootIntent);
PauseAllPlayersAndStopSelf();
}

public BoundServiceBinder? Binder { get; private set; }
public NotificationManager? NotificationManager { get; private set; }
public NotificationManager? NotificationManager { get; 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;

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

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

StartForeground(nOTIFICATION_ID, CreateNotification());

var audioAttribute = new AndroidX.Media3.Common.AudioAttributes.Builder()?
.SetContentType(C.AudioContentTypeMusic)? // When phonecalls come in, music is paused
.SetUsage(C.UsageMedia)?
.Build();

var 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.SelectionReasonUnknown); // 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.Build() 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");
}

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

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

StopSelf();
PauseAllPlayersAndStopSelf();
}

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

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

var style = new MediaStyleNotificationHelper.MediaStyle(session);
if (!OperatingSystem.IsAndroidVersionAtLeast(33))
var channel = new Android.App.NotificationChannel(cHANNEL_ID, "Media Playback", NotificationImportance.Low)
{
SetLegacyNotifications(session, mediaElement);
}

notificationBuilder.SetStyle(style);
NotificationManagerCompat.From(Platform.AppContext)?.Notify(1, notificationBuilder.Build());
Description = "Media playback controls",
Name = "Media Playback"
};
channel.SetShowBadge(false);
channel.LockscreenVisibility = NotificationVisibility.Public;
NotificationManager manager = GetSystemService(NotificationService) as NotificationManager ?? throw new InvalidOperationException($"{nameof(NotificationManager)} cannot be null");
manager.CreateNotificationChannel(channel);
}

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

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.SessionCompatToken);
playerNotificationManager.SetPlayer(mediaElement);
playerNotificationManager.SetColorized(true);
playerNotificationManager.SetShowPlayButtonIfPlaybackIsSuppressed(true);
playerNotificationManager.SetSmallIcon(Resource.Drawable.media3_notification_small_icon);
playerNotificationManager.SetPriority(NotificationCompat.PriorityDefault);
playerNotificationManager.SetUseChronometer(true);
return new NotificationCompat.Builder(Platform.AppContext ?? throw new InvalidOperationException("AppContext cannot be null"), cHANNEL_ID)
.SetContentTitle("Playing Media")?
.SetContentText("Artist")?
.SetSmallIcon(Resource.Drawable.notification_bg_low)?
.SetVisibility((int)NotificationVisibility.Public)?
.SetOngoing(true)?
.Build() ?? throw new InvalidOperationException("Notification cannot be null");
}

protected override void Dispose(bool disposing)
public override MediaSession? OnGetSession(MediaSession.ControllerInfo? p0)
{
if (!isDisposed)
{
if (disposing)
{
NotificationManager?.Dispose();
NotificationManager = null;

playerNotificationManager?.Dispose();
playerNotificationManager = null;

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

StopSelf();
}

isDisposed = true;
}

base.Dispose(disposing);
return mediaSession;
}

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

[MemberNotNull(nameof(notificationBuilder), nameof(NotificationManager))]
void StartForegroundServices()
{
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());
}
}
}
Loading
Loading