Skip to content
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e6ba3b1
Fix playback state update in PlatformUpdateSource method
ne0rrmatrix Feb 9, 2026
3068069
Update player state handling to use Media3 constants
ne0rrmatrix Feb 10, 2026
a56c0ec
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
fbc466a
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
7e85726
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
b4b73cf
Update player state handling to use Media3 constants
ne0rrmatrix Feb 10, 2026
c2c08f0
Refactor playback state management.
ne0rrmatrix Feb 10, 2026
9f7e621
Add null check
ne0rrmatrix Feb 10, 2026
ff01b96
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
957d2bf
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
a1d3f2f
Update playback state handling and error reporting
ne0rrmatrix Feb 10, 2026
b1e750f
Update src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.andr…
ne0rrmatrix Feb 10, 2026
d342148
Fix failed state bug
ne0rrmatrix Feb 11, 2026
f02e88f
Refactor player state handling and error reporting in MediaManager
ne0rrmatrix Feb 11, 2026
ad2e317
Refactor error handling and update source logic in MediaManager
ne0rrmatrix Feb 11, 2026
66509cc
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Feb 11, 2026
b495897
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Feb 12, 2026
3dcaf3e
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Feb 17, 2026
284701b
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Feb 27, 2026
0c75755
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Mar 8, 2026
b35e541
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Mar 11, 2026
e136b4c
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Mar 12, 2026
4ae7956
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Mar 18, 2026
79d4e3d
Merge branch 'main' into FixAutoPlay
ne0rrmatrix Mar 21, 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
168 changes: 65 additions & 103 deletions src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Android.App;
using Android.Content;
using Android.Util;
using Android.Views;
Expand All @@ -23,10 +22,6 @@ namespace CommunityToolkit.Maui.Core.Views;

public partial class MediaManager : Java.Lang.Object, IPlayerListener
{
const int bufferState = 2;
const int readyState = 3;
const int endedState = 4;

static readonly HttpClient client = new();
readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1);
bool isAndroidForegroundServiceEnabled = false;
Expand Down Expand Up @@ -77,53 +72,80 @@ public void UpdateNotifications()
}

/// <summary>
/// Occurs when ExoPlayer changes the player state.
/// Handles player event notifications and updates the media element state accordingly.
/// </summary>
/// <paramref name="playWhenReady">Indicates whether the player should start playing the media whenever the media is ready.</paramref>
/// <paramref name="playbackState">The state that the player has transitioned to.</paramref>
/// <remarks>
/// This is part of the <see cref="IPlayerListener"/> implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
/// </remarks>
public void OnPlayerStateChanged(bool playWhenReady, int playbackState)
/// <remarks>This method updates the media element's state and position in response to playback-related events
/// from the player. If a player error is present, it is handled before any state updates. No action is taken if either
/// the player, the media source, or the event set is null.</remarks>
/// <param name="player">The player instance that raised the events. If null, the method does nothing.</param>
/// <param name="playerEvents">The set of player events to process. If null, the method does nothing.</param>
public void OnEvents(IPlayer? player, PlayerEvents? playerEvents)
{
if (Player is null || MediaElement.Source is null)
if (player is null || MediaElement.Source is null || playerEvents is null)
{
return;
}

var newState = playbackState switch
// If there's a player error, bubble it through the existing error handler
if (player.PlayerError is not null)
{
PlaybackState.StateFastForwarding
or PlaybackState.StateRewinding
or PlaybackState.StateSkippingToNext
or PlaybackState.StateSkippingToPrevious
or PlaybackState.StateSkippingToQueueItem
or PlaybackState.StatePlaying => playWhenReady
? MediaElementState.Playing
: MediaElementState.Paused,
OnPlayerError(player.PlayerError);
return;
}

PlaybackState.StatePaused => MediaElementState.Paused,
// If playback state or playWhenReady changed, update UI together using values from the player
var playbackStateEvent = BasePlayer.InterfaceConsts.EventPlaybackStateChanged;
var playWhenReadyEvent = BasePlayer.InterfaceConsts.EventPlayWhenReadyChanged;

PlaybackState.StateConnecting
or PlaybackState.StateBuffering => MediaElementState.Buffering,
if (playerEvents.Contains(playbackStateEvent) || playerEvents.Contains(playWhenReadyEvent))
{
var playbackState = player.PlaybackState;
var playWhenReady = player.PlayWhenReady;

var newState = playbackState switch
{
BasePlayer.InterfaceConsts.StateBuffering => MediaElementState.Buffering,
BasePlayer.InterfaceConsts.StateReady => playWhenReady ? MediaElementState.Playing : MediaElementState.Paused,
BasePlayer.InterfaceConsts.StateEnded => MediaElementState.Stopped,
BasePlayer.InterfaceConsts.StateIdle => MediaElement.CurrentState is MediaElementState.None or MediaElementState.Failed or MediaElementState.Opening ? MediaElement.CurrentState : MediaElementState.Stopped,
_ => MediaElementState.None,
};

PlaybackState.StateNone => MediaElementState.None,
PlaybackState.StateStopped => MediaElement.CurrentState is not MediaElementState.Failed
? MediaElementState.Stopped
: MediaElementState.Failed,
if (playbackState == BasePlayer.InterfaceConsts.StateReady)
{
seekToTaskCompletionSource?.TrySetResult();
if (Player is not null)
{
newState = Player.PlayWhenReady ? MediaElementState.Playing : MediaElementState.Paused;
}
MediaElement.MediaOpened();
}

PlaybackState.StateError => MediaElementState.Failed,
if (playbackState == BasePlayer.InterfaceConsts.StateEnded)
{
MediaElement.MediaEnded();
}
MediaElement.CurrentStateChanged(newState);

_ => MediaElementState.None,
};
if (playbackState == BasePlayer.InterfaceConsts.StateReady)
{
MediaElement.Duration = TimeSpan.FromMilliseconds(player.Duration < 0 ? 0 : player.Duration);
MediaElement.Position = TimeSpan.FromMilliseconds(player.CurrentPosition < 0 ? 0 : player.CurrentPosition);
}
}
}

MediaElement.CurrentStateChanged(newState);
if (playbackState is readyState)
/// <summary>
/// Handles changes to the player error state by updating the media element's state to indicate a failure.
/// </summary>
/// <param name="error">The playback error that occurred, or null if the error state has been cleared.</param>
public void OnPlayerErrorChanged(PlaybackException? error)
{
if (error is null)
{
MediaElement.Duration = TimeSpan.FromMilliseconds(Player.Duration < 0 ? 0 : Player.Duration);
MediaElement.Position = TimeSpan.FromMilliseconds(Player.CurrentPosition < 0 ? 0 : Player.CurrentPosition);
return;
}
OnPlayerError(error);
}

/// <summary>
Expand Down Expand Up @@ -182,39 +204,6 @@ or PlaybackState.StateSkippingToQueueItem
return (Player, PlayerView);
}

/// <summary>
/// Occurs when ExoPlayer changes the playback state.
/// </summary>
/// <paramref name="playbackState">The state that the player has transitioned to.</paramref>
/// <remarks>
/// This is part of the <see cref="IPlayerListener"/> implementation.
/// While this method does not seem to have any references, it's invoked at runtime.
/// </remarks>
public void OnPlaybackStateChanged(int playbackState)
{
if (MediaElement.Source is null)
{
return;
}

MediaElementState newState = MediaElement.CurrentState;
switch (playbackState)
{
case bufferState:
newState = MediaElementState.Buffering;
break;
case endedState:
newState = MediaElementState.Stopped;
MediaElement.MediaEnded();
break;
case readyState:
seekToTaskCompletionSource?.TrySetResult();
break;
}

MediaElement.CurrentStateChanged(newState);
}

/// <summary>
/// Occurs when ExoPlayer encounters an error.
/// </summary>
Expand Down Expand Up @@ -252,7 +241,6 @@ public void OnPlayerError(PlaybackException? error)
}.Where(static s => !string.IsNullOrEmpty(s)));

MediaElement.MediaFailed(new MediaFailedEventArgs(message));

Logger.LogError("{LogMessage}", message);
}

Expand Down Expand Up @@ -369,30 +357,23 @@ protected virtual async partial ValueTask PlatformUpdateSource()
return;
}

MediaElement.CurrentStateChanged(MediaElementState.Opening);
Player.PlayWhenReady = MediaElement.ShouldAutoPlay;
cancellationTokenSource ??= new();
MediaElement.CurrentStateChanged(MediaElementState.Opening);
// ConfigureAwait(true) is required to prevent crash on startup
var result = await SetPlayerData(cancellationTokenSource.Token).ConfigureAwait(true);
var item = result?.Build();

if (item?.MediaMetadata is not null)
if (item is not null)
{
Player.SetMediaItem(item);
Player.Prepare();
hasSetSource = true;
}

if (hasSetSource)
if (hasSetSource && isAndroidForegroundServiceEnabled)
{
if (Player.PlayerError is null)
{
MediaElement.MediaOpened();
}
if (isAndroidForegroundServiceEnabled)
{
UpdateNotifications();
}
UpdateNotifications();
}
}

Expand Down Expand Up @@ -742,18 +723,18 @@ public void OnAvailableCommandsChanged(PlayerCommands? player) { }
public void OnCues(CueGroup? cues) { }
public void OnDeviceInfoChanged(DeviceInfo? deviceInfo) { }
public void OnDeviceVolumeChanged(int volume, bool muted) { }
public void OnEvents(IPlayer? player, PlayerEvents? playerEvents) { }
public void OnIsLoadingChanged(bool isLoading) { }
public void OnIsPlayingChanged(bool isPlaying) { }
public void OnLoadingChanged(bool isLoading) { }
public void OnMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) { }
public void OnMediaItemTransition(MediaItem? mediaItem, int reason) { }
public void OnMediaMetadataChanged(MediaMetadata? mediaMetadata) { }
public void OnMetadata(Metadata? metadata) { }
public void OnPlaybackStateChanged(int playbackState) { }
public void OnPlayWhenReadyChanged(bool playWhenReady, int reason) { }
public void OnPositionDiscontinuity(PlayerPositionInfo? oldPosition, PlayerPositionInfo? newPosition, int reason) { }
public void OnPlaybackSuppressionReasonChanged(int playbackSuppressionReason) { }
public void OnPlayerErrorChanged(PlaybackException? error) { }
public void OnPlayerStateChanged(bool playWhenReady, int playbackState) { }
public void OnPlaylistMetadataChanged(MediaMetadata? mediaMetadata) { }
public void OnRenderedFirstFrame() { }
public void OnRepeatModeChanged(int repeatMode) { }
Expand All @@ -766,23 +747,4 @@ public void OnTimelineChanged(Timeline? timeline, int reason) { }
public void OnTrackSelectionParametersChanged(TrackSelectionParameters? trackSelectionParameters) { }
public void OnTracksChanged(Tracks? tracks) { }
#endregion

static class PlaybackState
{
public const int StateBuffering = 6;
public const int StateConnecting = 8;
public const int StateFailed = 7;
public const int StateFastForwarding = 4;
public const int StateNone = 0;
public const int StatePaused = 2;
public const int StatePlaying = 3;
public const int StateRewinding = 5;
public const int StateSkippingToNext = 10;
public const int StateSkippingToPrevious = 9;
public const int StateSkippingToQueueItem = 11;
public const int StateStopped = 1;
public const int StateError = 7;
}


}
Loading