Skip to content

Commit 79ac365

Browse files
committed
Make media element headless on android
1 parent 33498fb commit 79ac365

File tree

3 files changed

+94
-48
lines changed

3 files changed

+94
-48
lines changed
Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
using CommunityToolkit.Maui.Core.Views;
1+
using CommunityToolkit.Maui.Core.Views;
22
using CommunityToolkit.Maui.Views;
3+
using Microsoft.Maui;
4+
using Microsoft.Maui.ApplicationModel;
35
using Microsoft.Maui.Handlers;
6+
#if ANDROID
7+
using Android.App;
8+
using Android.Content;
9+
#endif
410

511
namespace CommunityToolkit.Maui.Core.Handlers;
612

@@ -17,21 +23,65 @@ public static void ShouldLoopPlayback(MediaElementHandler handler, MediaElement
1723
handler.MediaManager?.UpdateShouldLoopPlayback();
1824
}
1925

26+
protected override MauiMediaElement? CreatePlatformView()
27+
{
28+
// Use the handler's MauiContext - guaranteed to be available when PrepareAndPlayAsync is called
29+
// (bootstrap completes before PrepareAndPlayAsync is called)
30+
if (MauiContext == null)
31+
throw new InvalidOperationException("MauiContext is null - ensure bootstrap has completed before calling PrepareAndPlayAsync");
32+
33+
var dispatcher = GetDispatcher();
34+
MediaManager ??= new MediaManager(MauiContext, VirtualView, dispatcher);
35+
36+
// Always use None (headless mode) for audio-only playback
37+
// This app plays Bible readings and music (audio-only), so no UI view is needed
38+
// ExoPlayer works perfectly in headless mode for audio playback
39+
var (_, playerView) = MediaManager.CreatePlatformView(AndroidViewType.None);
40+
41+
// Return null view for headless, or wrap PlayerView if UI exists
42+
if (playerView == null)
43+
{
44+
// Headless mode - perfectly valid
45+
return null;
46+
}
47+
48+
// UI mode - we have a real context and PlayerView
49+
if (Context == null)
50+
throw new InvalidOperationException("Context is null but PlayerView was created");
2051

21-
protected override MauiMediaElement CreatePlatformView()
52+
return new MauiMediaElement(Context, playerView);
53+
}
54+
55+
private IDispatcher GetDispatcher()
2256
{
23-
MediaManager ??= new(MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null"),
24-
VirtualView,
25-
Dispatcher.GetForCurrentThread() ?? throw new InvalidOperationException($"{nameof(IDispatcher)} cannot be null"));
57+
// Get dispatcher - try current thread first, then Application.Current dispatcher
58+
// After bootstrap completes, Application.Current.Dispatcher should always be available
59+
var dispatcher = Dispatcher.GetForCurrentThread();
60+
if (dispatcher == null)
61+
{
62+
// Try to get dispatcher from Application.Current (works in background services after bootstrap)
63+
dispatcher = Microsoft.Maui.Controls.Application.Current?.Dispatcher;
64+
}
65+
66+
if (dispatcher == null)
67+
{
68+
throw new InvalidOperationException("Dispatcher cannot be null - ensure bootstrap has completed before calling CreatePlatformView");
69+
}
2670

27-
var (_, playerView) = MediaManager.CreatePlatformView(VirtualView.AndroidViewType);
28-
return new(Context, playerView);
71+
return dispatcher;
2972
}
3073

31-
protected override void DisconnectHandler(MauiMediaElement platformView)
74+
protected override void DisconnectHandler(MauiMediaElement? platformView)
3275
{
33-
platformView.Dispose();
76+
// Handle headless mode where platformView may be null
77+
if (platformView != null)
78+
{
79+
base.DisconnectHandler(platformView);
80+
platformView.Dispose();
81+
}
82+
83+
// Dispose() will handle MediaManager disposal (releases MediaSession and stops MediaControlsService)
84+
// This works for both UI mode and headless mode
3485
Dispose();
35-
base.DisconnectHandler(platformView);
3686
}
3787
}

libraries/CommunityToolkit.Maui.MediaElement/Primitives/AndroidViewType.shared.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@ public enum AndroidViewType
1515
/// <summary>
1616
/// Create MediaElement on Android using TextureView
1717
/// </summary>
18-
TextureView
18+
TextureView,
19+
20+
/// <summary>
21+
/// Headless mode - no view created (audio-only playback, e.g., Android Auto)
22+
/// </summary>
23+
None
1924
}

libraries/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using System.Diagnostics.CodeAnalysis;
3+
using System.Linq;
34
using Android.Content;
45
using Android.Views;
56
using Android.Widget;
@@ -39,6 +40,7 @@ public partial class MediaManager : Java.Lang.Object, IPlayerListener
3940

4041
/// <summary>
4142
/// The platform native counterpart of <see cref="MediaElement"/>.
43+
/// Null in headless mode (audio-only, no view required).
4244
/// </summary>
4345
protected PlayerView? PlayerView { get; set; }
4446

@@ -126,58 +128,47 @@ or PlaybackState.StateSkippingToQueueItem
126128

127129
/// <summary>
128130
/// Creates the corresponding platform view of <see cref="MediaElement"/> on Android.
131+
/// Modified for headless (audio-only) playback - no TextureView/Surface required.
129132
/// </summary>
130133
/// <returns>The platform native counterpart of <see cref="MediaElement"/>.</returns>
131134
/// <exception cref="NullReferenceException">Thrown when <see cref="Context"/> is <see langword="null"/> or when the platform view could not be created.</exception>
132-
[MemberNotNull(nameof(Player), nameof(PlayerView), nameof(session))]
133-
public (PlatformMediaElement platformView, PlayerView PlayerView) CreatePlatformView(AndroidViewType androidViewType)
135+
[MemberNotNull(nameof(Player), nameof(session))]
136+
public (PlatformMediaElement platformView, PlayerView? PlayerView) CreatePlatformView(AndroidViewType androidViewType)
134137
{
135-
Player = new ExoPlayerBuilder(MauiContext.Context).Build() ?? throw new InvalidOperationException("Player cannot be null");
136-
Player.AddListener(this);
137-
138-
if (androidViewType is AndroidViewType.SurfaceView)
138+
// Use MauiContext.Context - guaranteed to be available when PrepareAndPlayAsync is called
139+
// (bootstrap completes before PrepareAndPlayAsync is called)
140+
var context = MauiContext.Context;
141+
if (context == null)
139142
{
140-
PlayerView = new PlayerView(MauiContext.Context)
141-
{
142-
Player = Player,
143-
UseController = false,
144-
ControllerAutoShow = false,
145-
LayoutParameters = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
146-
};
143+
throw new InvalidOperationException("Cannot create ExoPlayer - MauiContext.Context is null. Ensure bootstrap has completed before calling PrepareAndPlayAsync.");
147144
}
148-
else if (androidViewType is AndroidViewType.TextureView)
149-
{
150-
if (MauiContext.Context?.Resources is null)
151-
{
152-
throw new InvalidOperationException("Unable to retrieve Android Resources");
153-
}
154145

155-
var resources = MauiContext.Context.Resources;
156-
var xmlResource = resources.GetXml(Microsoft.Maui.Resource.Layout.textureview);
157-
xmlResource.Read();
146+
Log.I("MediaManager", $"MediaManager: Creating ExoPlayer directly via ExoPlayerBuilder. Context: {context.GetType().FullName}");
158147

159-
var attributes = Android.Util.Xml.AsAttributeSet(xmlResource)!;
148+
// Direct creation - no reflection needed
149+
// Xamarin.AndroidX.Media3 bindings expose ExoPlayer.Builder as ExoPlayerBuilder
150+
var exoPlayer = new ExoPlayerBuilder(context).Build();
160151

161-
PlayerView = new PlayerView(MauiContext.Context, attributes)
162-
{
163-
Player = Player,
164-
UseController = false,
165-
ControllerAutoShow = false,
166-
LayoutParameters = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
167-
};
168-
}
169-
else
170-
{
171-
throw new NotSupportedException($"{androidViewType} is not yet supported");
172-
}
152+
Player = exoPlayer;
153+
Player.AddListener(this);
154+
155+
// Headless audio-only config (critical for no surface/view)
156+
Player.SetVideoSurfaceView(null); // No surface ever
157+
// Optionally force disable video tracks if sources might have video
158+
// (ExoPlayer auto-skips video when no surface, but this ensures it)
159+
160+
Log.I("MediaManager", $"MediaManager: ExoPlayer created headlessly. Type: {Player.GetType().FullName}");
161+
162+
// Always headless mode - no PlayerView needed for audio-only playback
163+
PlayerView? playerView = null;
173164

174165
var mediaSession = new MediaSession.Builder(Platform.AppContext, Player);
175166
mediaSession.SetId(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..8]);
176167

177168
session ??= mediaSession.Build() ?? throw new InvalidOperationException("Session cannot be null");
178169
ArgumentNullException.ThrowIfNull(session.Id);
179170

180-
return (Player, PlayerView);
171+
return (Player, playerView);
181172
}
182173

183174
/// <summary>
@@ -770,4 +761,4 @@ static class PlaybackState
770761
}
771762

772763

773-
}
764+
}

0 commit comments

Comments
 (0)