diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs index ce5f3174ef..cdf7b42c1e 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Android.App; using Android.Content; using Android.Util; using Android.Views; @@ -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; @@ -77,53 +72,80 @@ public void UpdateNotifications() } /// - /// Occurs when ExoPlayer changes the player state. + /// Handles player event notifications and updates the media element state accordingly. /// - /// Indicates whether the player should start playing the media whenever the media is ready. - /// The state that the player has transitioned to. - /// - /// This is part of the implementation. - /// While this method does not seem to have any references, it's invoked at runtime. - /// - public void OnPlayerStateChanged(bool playWhenReady, int playbackState) + /// 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. + /// The player instance that raised the events. If null, the method does nothing. + /// The set of player events to process. If null, the method does nothing. + 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) + /// + /// Handles changes to the player error state by updating the media element's state to indicate a failure. + /// + /// The playback error that occurred, or null if the error state has been cleared. + 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); } /// @@ -182,39 +204,6 @@ or PlaybackState.StateSkippingToQueueItem return (Player, PlayerView); } - /// - /// Occurs when ExoPlayer changes the playback state. - /// - /// The state that the player has transitioned to. - /// - /// This is part of the implementation. - /// While this method does not seem to have any references, it's invoked at runtime. - /// - 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); - } - /// /// Occurs when ExoPlayer encounters an error. /// @@ -252,7 +241,6 @@ public void OnPlayerError(PlaybackException? error) }.Where(static s => !string.IsNullOrEmpty(s))); MediaElement.MediaFailed(new MediaFailedEventArgs(message)); - Logger.LogError("{LogMessage}", message); } @@ -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(); } } @@ -742,7 +723,6 @@ 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) { } @@ -750,10 +730,11 @@ 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) { } @@ -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; - } - - } \ No newline at end of file