Skip to content

[BUG] iOS/macOS MediaElement.MediaOpened fires before AVPlayerItem is actually ReadyToPlay #3249

Description

@jonmdev

Is there an existing issue for this?

  • I have searched the existing issues

Did you read the "Reporting a bug" section on Contributing file?

Current Behavior

Summary

On iOS/macOS, CommunityToolkit.Maui.MediaElement appears to raise MediaOpened too early.

The current iOS/macOS MediaManager.PlatformUpdateSource() raises MediaElement.MediaOpened() after creating an AVPlayerItem and calling Player.ReplaceCurrentItemWithPlayerItem(PlayerItem), as long as PlayerItem is not null && PlayerItem.Error is null.

That is not equivalent to the native media item being ready.

For AVFoundation, the correct readiness signal is:

AVPlayerItem.Status == AVPlayerItemStatus.ReadyToPlay

new AVPlayerItem(asset) and PlayerItem.Error is null only mean that an item object exists and has not failed yet. They do not mean the media has loaded, has a valid duration, or is ready to play.

Observed behavior

In a .NET MAUI app using multiple MediaElement instances with local embedded resource videos, where I was debugging the AVPlayer state via Reflection:

ShouldAutoPlay = false;
ShouldShowPlaybackControls = false;
Source = MediaSource.FromResource(...);

all six MediaOpened events fired, causing app-level readiness to become true:

MEDIA OPENED PT1 0
MEDIA OPENED PT2 0
MEDIA OPENED PT2 2
MEDIA OPENED PT1 2
MEDIA OPENED PT2 1
MEDIA OPENED PT1 1
READY ready=True opened=111/111

But at the moment READY was reached, every MediaElement was still reported as Opening with zero duration:

pt1=0:Opening@0/0,1:Opening@0/0,2:Opening@0/0
pt2=0:Opening@0/0,1:Opening@0/0,2:Opening@0/0

Calling Play() or Stop() after this false readiness caused the players to fail:

pt1=0:Failed@0/0,1:Failed@0/0,2:Failed@0/0
pt2=0:Failed@0/0,1:Failed@0/0,2:Failed@0/0

So MediaOpened was raised while the media was not actually opened/ready.

Expected behavior

MediaOpened should fire only when the native AVPlayerItem reaches:

AVPlayerItemStatus.ReadyToPlay

At that point, application code should be able to safely treat the media as opened and ready for playback operations such as:

Play();
Pause();
SeekTo(...);

It should not fire just because an AVPlayerItem object was created.

Affected file / class

File:

src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.ios.cs

Class:

CommunityToolkit.Maui.Core.Views.MediaManager

Relevant current logic:

PlayerItem = asset is not null
    ? new AVPlayerItem(asset)
    : null;

metaData.SetMetadata(PlayerItem, MediaElement);
CurrentItemErrorObserver?.Dispose();

Player.ReplaceCurrentItemWithPlayerItem(PlayerItem);

...

if (PlayerItem is not null && PlayerItem.Error is null)
{
    MediaElement.MediaOpened();

    (MediaElement.MediaWidth, MediaElement.MediaHeight) = await GetVideoDimensions(PlayerItem);

    if (MediaElement.ShouldAutoPlay)
    {
        Player.Play();
    }

    await SetPoster();
}

The problem is this line:

MediaElement.MediaOpened();

It is called before observing PlayerItem.Status == AVPlayerItemStatus.ReadyToPlay.

Root cause

PlayerItem.Error is null is not a readiness check.

Immediately after creating an AVPlayerItem, the item can still be in an unknown/opening state. AVFoundation exposes readiness through AVPlayerItem.Status, and the item should be observed until it reaches ReadyToPlay or Failed.

The Toolkit currently has player-level status observers:

StatusObserver = Player.AddObserver("status", ...);
TimeControlStatusObserver = Player.AddObserver("timeControlStatus", ...);

but MediaOpened should be driven by the current item’s status, not by item creation.

Proposed fix

Move MediaElement.MediaOpened() out of PlatformUpdateSource() and into an observer for the current AVPlayerItem.Status.

Add fields:

bool hasMediaOpened;
IDisposable? CurrentItemStatusObserver { get; set; }

Reset them when a new source is assigned:

hasMediaOpened = false;

CurrentItemStatusObserver?.Dispose();
CurrentItemStatusObserver = null;

After creating and replacing the current item, observe the item status:

PlayerItem = asset is not null
    ? new AVPlayerItem(asset)
    : null;

metaData.SetMetadata(PlayerItem, MediaElement);
CurrentItemErrorObserver?.Dispose();

Player.ReplaceCurrentItemWithPlayerItem(PlayerItem);

CurrentItemStatusObserver = PlayerItem?.AddObserver(
    "status",
    ValueObserverOptions,
    PlayerItemStatusChanged
);

Do not call MediaOpened() immediately after ReplaceCurrentItemWithPlayerItem.

Instead:

async void PlayerItemStatusChanged(NSObservedChange obj)
{
    if (PlayerItem is null)
    {
        return;
    }

    switch (PlayerItem.Status)
    {
        case AVPlayerItemStatus.ReadyToPlay:

            if (hasMediaOpened)
            {
                return;
            }

            hasMediaOpened = true;

            MediaElement.Duration = ConvertTime(PlayerItem.Duration);
            MediaElement.Position = ConvertTime(PlayerItem.CurrentTime);

            MediaElement.CurrentStateChanged(
                Player?.Rate > 0
                    ? MediaElementState.Playing
                    : MediaElementState.Paused
            );

            (MediaElement.MediaWidth, MediaElement.MediaHeight) = await GetVideoDimensions(PlayerItem);

            MediaElement.MediaOpened();

            if (MediaElement.ShouldAutoPlay)
            {
                Player?.Play();
            }

            await SetPoster();

            break;

        case AVPlayerItemStatus.Failed:

            var message = PlayerItem.Error is not null
                ? $"{PlayerItem.Error.LocalizedDescription} - {PlayerItem.Error.LocalizedFailureReason}"
                : "AVPlayerItem failed.";

            MediaElement.CurrentStateChanged(MediaElementState.Failed);
            MediaElement.MediaFailed(new MediaFailedEventArgs(message));
            Logger.LogError("{LogMessage}", message);

            break;
    }
}

Also dispose the observer:

CurrentItemStatusObserver?.Dispose();
CurrentItemStatusObserver = null;

inside Dispose(bool disposing).

Why this fixes the issue

This makes MediaOpened match its documented meaning: the media is loaded and ready to play.

It also aligns the Toolkit with AVFoundation’s native readiness model:

AVPlayerItemStatus.ReadyToPlay

After applying an app-side reflection workaround that ignored Toolkit MediaOpened on iOS and instead watched the underlying AVPlayer.CurrentItem.Status, the same media elements loaded correctly and playback worked. All behavior became predictable and normal while watching and reacting to the true AVPlayer events.

The workaround treated only this native state as opened:

player.CurrentItem.Status == AVPlayerItemStatus.ReadyToPlay

and used AVPlayerItem.Notifications.ObserveDidPlayToEndTime for ended playback. That resolved the false-ready / Opening@0/0 / Failed@0/0 behavior.

Minimal reproduction outline

  1. Create a .NET MAUI app using CommunityToolkit.Maui.MediaElement.
  2. Add one or more MediaElement instances on iOS.
  3. Configure:
ShouldAutoPlay = false;
ShouldShowPlaybackControls = false;
Aspect = Aspect.Fill;
  1. Attach handlers before setting Source:
mediaElement.MediaOpened += ...;
mediaElement.MediaFailed += ...;
  1. Set a local embedded video resource:
mediaElement.Source = MediaSource.FromResource("video.mp4");
  1. In the MediaOpened handler, log by reflection the AVPlayer state vs the MediaElement state. You can access the AVPlayer by running on mediaElement.HandlerChanged:
static AVPlayer? getAVPlayer(MediaElement mediaElement) {

    var handler = mediaElement.Handler;
    if (handler is null) {
        return null;
    }

    object? mediaManager = getPropertyOrFieldFromTypeTree(
        handler,
        "MediaManager"
    );

    if (mediaManager is null) {
        mediaManager = getPropertyOrFieldFromTypeTree(
            handler,
            "mediaManager"
        );
    }

    if (mediaManager is null) {
        return null;
    }

    object? player = getPropertyOrFieldFromTypeTree(
        mediaManager,
        "Player"
    );

    return player as AVPlayer;
}
static object? getPropertyOrFieldFromTypeTree(object target, string name) {

    Type? type = target.GetType();

    while (type != null) {

        PropertyInfo? property = type.GetProperty(
            name,
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
        );

        if (property != null) {
            return property.GetValue(target);
        }

        FieldInfo? field = type.GetField(
            name,
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
        );

        if (field != null) {
            return field.GetValue(target);
        }

        type = type.BaseType;
    }

    return null;
}
  1. Observe that MediaOpened can fire while the element is still:
Opening@0/0

and subsequent playback operations can fail.

Requested change

Please update iOS/macOS MediaManager.ios.cs so that:

  1. MediaOpened() is not raised immediately after AVPlayerItem creation.
  2. MediaOpened() is raised only once the current item reaches AVPlayerItemStatus.ReadyToPlay.
  3. Duration, Position, and media dimensions are updated around the actual ready transition.
  4. AVPlayerItemStatus.Failed triggers MediaFailed.
  5. Observers are reset/disposed correctly when changing source or disposing the player.

This would make iOS/macOS MediaOpened correctly represent native media readiness and match the documented MediaOpened contract.

Environment

- .NET MAUI CommunityToolkit:
- OS:
- .NET MAUI:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions