Compartir a través de


Creación de un control personalizado mediante controladores

Browse sample. Examina la muestra.

Un requisito estándar para las aplicaciones es la capacidad de reproducir vídeos. En este artículo se examina cómo crear un control Video multiplataforma de .NET Multi-platform App UI (.NET MAUI) que usa un controlador para asignar la API de control multiplataforma a las vistas nativas de Android, iOS y Mac Catalyst que reproducen vídeos. Este control puede reproducir vídeo desde tres orígenes:

  • Una dirección URL, que representa un vídeo remoto.
  • Un recurso, que es un archivo incrustado en la aplicación.
  • Un archivo, desde la biblioteca de vídeos del dispositivo.

Los controles de vídeo necesitan controles de transporte, que son botones para reproducir y pausar el vídeo, y una barra de posición que muestra el progreso a través del vídeo y permite al usuario ir rápidamente a una ubicación diferente. El control Video puede usar los controles de transporte y la barra de posición proporcionados por la plataforma, o puedes proporcionar controles de transporte personalizados y una barra de posición. En las capturas de pantalla siguientes se muestra el control en iOS, con y sin controles de transporte personalizados:

Screenshot of video playback on iOS.Screenshot of video playback using custom transport controls on iOS.

Un control de vídeo más sofisticado tendría algunas características adicionales, como un control de volumen, un mecanismo para interrumpir la reproducción de vídeo cuando entra una llamada telefónica y una manera de mantener la pantalla activa durante la reproducción.

La arquitectura del control Video se muestra en el diagrama siguiente:

Video handler architecture.

La clase Video proporciona la API multiplataforma para el control. La asignación de la API multiplataforma a las API de vista nativa se realiza mediante la clase VideoHandler en cada plataforma, que asigna la clase Video a la clase MauiVideoPlayer. En iOS y Mac Catalyst, la clase MauiVideoPlayer usa el tipo AVPlayer para proporcionar reproducción de vídeo. En Android, la clase MauiVideoPlayer usa el tipo VideoView para proporcionar reproducción de vídeo. En Windows, la clase MauiVideoPlayer usa el tipo MediaPlayerElement para proporcionar reproducción de vídeo.

Importante

.NET MAUI desacopla sus controladores de sus controles multiplataforma a través de interfaces. Esto permite que los marcos experimentales como Comet y Fabulous proporcionen sus propios controles multiplataforma, que implementan las interfaces, mientras siguen usando los controladores de .NET MAUI. La creación de una interfaz para el control multiplataforma solo es necesaria si necesitas desacoplar el controlador de tu control multiplataforma para un propósito similar o con fines de prueba.

El proceso para crear un control personalizado de .NET MAUI multiplataforma, cuyas implementaciones de plataforma se proporcionan mediante controladores, es el siguiente:

  1. Crea una clase para el control multiplataforma, que proporciona la API pública del control. Para obtener más información, consulta Creación del control multiplataforma.
  2. Crea cualquier tipo de multiplataforma adicional necesario.
  3. Crea una clase de controlador partial. Para más información, consulta Creación del controlador.
  4. En la clase de controlador, crea un diccionario PropertyMapper, que define las acciones que se realizarán cuando se produzcan cambios en las propiedades de multiplataforma. Para obtener más información, consulta Creación del asignador de propiedades.
  5. Opcionalmente, en la clase de controlador, crea un diccionario CommandMapper, que define las acciones que se deben realizar cuando el control de multiplataforma envía instrucciones a las vistas nativas que implementan el control multiplataforma. Para obtener más información, consulta Creación del asignador de comandos.
  6. Crea clases de controlador partial para cada plataforma que cree las vistas nativas que implementan el control multiplataforma. Para obtener más información, consulta Creación de los controles de plataforma.
  7. Registra el controlador mediante los métodos ConfigureMauiHandlers y AddHandler en la clase MauiProgram de la aplicación. Para más información, consulta Registro del controlador.

Después, se puede consumir el control multiplataforma. Para más información, consulta Constumo del control multiplataforma.

Creación del control multiplataforma

Para crear un control multiplataforma, debes crear una clase que derive de View:

using System.ComponentModel;

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        public static readonly BindableProperty AreTransportControlsEnabledProperty =
            BindableProperty.Create(nameof(AreTransportControlsEnabled), typeof(bool), typeof(Video), true);

        public static readonly BindableProperty SourceProperty =
            BindableProperty.Create(nameof(Source), typeof(VideoSource), typeof(Video), null);

        public static readonly BindableProperty AutoPlayProperty =
            BindableProperty.Create(nameof(AutoPlay), typeof(bool), typeof(Video), true);

        public static readonly BindableProperty IsLoopingProperty =
            BindableProperty.Create(nameof(IsLooping), typeof(bool), typeof(Video), false);            

        public bool AreTransportControlsEnabled
        {
            get { return (bool)GetValue(AreTransportControlsEnabledProperty); }
            set { SetValue(AreTransportControlsEnabledProperty, value); }
        }

        [TypeConverter(typeof(VideoSourceConverter))]
        public VideoSource Source
        {
            get { return (VideoSource)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        public bool AutoPlay
        {
            get { return (bool)GetValue(AutoPlayProperty); }
            set { SetValue(AutoPlayProperty, value); }
        }

        public bool IsLooping
        {
            get { return (bool)GetValue(IsLoopingProperty); }
            set { SetValue(IsLoopingProperty, value); }
        }        
        ...
    }
}

El control debe proporcionar una API pública a la que accederán sus controladores y controlará los consumidores. Los controles multiplataforma deben derivar de View, que representa un elemento visual que se usa para colocar diseños y vistas en la pantalla.

Creación del controlador

Después de crear el control multiplataforma, debes crear una clase partial para el controlador:

#if IOS || MACCATALYST
using PlatformView = VideoDemos.Platforms.MaciOS.MauiVideoPlayer;
#elif ANDROID
using PlatformView = VideoDemos.Platforms.Android.MauiVideoPlayer;
#elif WINDOWS
using PlatformView = VideoDemos.Platforms.Windows.MauiVideoPlayer;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif
using VideoDemos.Controls;
using Microsoft.Maui.Handlers;

namespace VideoDemos.Handlers
{
    public partial class VideoHandler
    {
    }
}

La clase de controlador es una clase parcial cuya implementación se completará en cada plataforma con una clase parcial adicional.

Las instrucciones using condicionales definen el tipo PlatformView en cada plataforma. En Android, iOS, Mac Catalyst y Windows, la clase personalizada MauiVideoPlayer proporciona las vistas nativas. La instrucción condicional using final define PlatformView que es igual a System.Object. Esto es necesario para que el tipo PlatformView se pueda usar en el controlador para su utilización en todas las plataformas. La alternativa sería tener que definir la propiedad PlatformView una vez por plataforma mediante compilación condicional.

Creación del asignador de propiedades

Cada controlador normalmente proporciona un asignador de propiedades, que define qué acciones realizar cuando se produce un cambio de propiedad en el control multiplataforma. El tipo PropertyMapper es un Dictionary que asigna las propiedades del control multiplataforma a sus acciones asociadas.

PropertyMapper se define en la clase genérica ViewHandler de .NET MAUI y requiere que se proporcionen dos argumentos genéricos:

  • La clase para el control multiplataforma, que se deriva de View.
  • La clase para el controlador.

En el ejemplo de código siguiente se muestra la clase extendida VideoHandler con la definición PropertyMapper:

public partial class VideoHandler
{
    public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
    {
        [nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
        [nameof(Video.Source)] = MapSource,
        [nameof(Video.IsLooping)] = MapIsLooping,
        [nameof(Video.Position)] = MapPosition
    };

    public VideoHandler() : base(PropertyMapper)
    {
    }
}

PropertyMapper es un Dictionary cuya clave es string y cuyo valor es un Action genérico. string representa el nombre de la propiedad del control multiplataforma y Action representa un método static que requiere el controlador y el control multiplataforma como argumentos. Por ejemplo, la firma del método MapSource es public static void MapSource(VideoHandler handler, Video video).

Cada controlador de plataforma debe proporcionar implementaciones de las acciones, que manipulan las API de vista nativa. Esto garantiza que, cuando se establece una propiedad en un control multiplataforma, la vista nativa subyacente se actualice según sea necesario. La ventaja de este enfoque es que permite una fácil personalización del control multiplataforma, ya que el asignador de propiedades se puede modificar mediante consumidores de control multiplataforma sin subclases.

Creación del asignador de comandos

Cada controlador también puede proporcionar un asignador de comandos, que define las acciones que se deben realizar cuando el control multiplataforma envía comandos a vistas nativas. Los asignadores de comandos son similares a los asignadores de propiedades, pero permiten pasar datos adicionales. En este contexto, un comando es una instrucción y, opcionalmente, sus datos, que se envían a una vista nativa. El tipo CommandMapper es un Dictionary que asigna miembros de control multiplataforma a sus acciones asociadas.

CommandMapper se define en la clase genérica ViewHandler de .NET MAUI y requiere que se proporcionen dos argumentos genéricos:

  • La clase para el control multiplataforma, que se deriva de View.
  • La clase para el controlador.

En el ejemplo de código siguiente se muestra la clase extendida VideoHandler con la definición CommandMapper:

public partial class VideoHandler
{
    public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
    {
        [nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
        [nameof(Video.Source)] = MapSource,
        [nameof(Video.IsLooping)] = MapIsLooping,
        [nameof(Video.Position)] = MapPosition
    };

    public static CommandMapper<Video, VideoHandler> CommandMapper = new(ViewCommandMapper)
    {
        [nameof(Video.UpdateStatus)] = MapUpdateStatus,
        [nameof(Video.PlayRequested)] = MapPlayRequested,
        [nameof(Video.PauseRequested)] = MapPauseRequested,
        [nameof(Video.StopRequested)] = MapStopRequested
    };

    public VideoHandler() : base(PropertyMapper, CommandMapper)
    {
    }
}

CommandMapper es un Dictionary cuya clave es un string y cuyo valor es un Action genérico. string representa el nombre del comando del control multiplataforma y Action representa un método static que requiere el controlador, el control multiplataforma y los datos opcionales como argumentos. Por ejemplo, la firma del método MapPlayRequested es public static void MapPlayRequested(VideoHandler handler, Video video, object? args).

Cada controlador de plataforma debe proporcionar implementaciones de las acciones, que manipulan las API de vista nativa. Esto garantiza que, cuando se envía un comando desde el control multiplataforma, la vista nativa subyacente se manipulará según sea necesario. La ventaja de este enfoque es que elimina la necesidad de que las vistas nativas se suscriban a eventos de control multiplataforma y cancelen la suscripción a estos. Además, permite una personalización sencilla, ya que el asignador de comandos se puede modificar mediante consumidores de control multiplataforma sin subclases.

Creación de los controles de plataforma

Después de crear los asignadores para el controlador, debes proporcionar implementaciones de controlador en todas las plataformas. Esto se puede lograr agregando implementaciones de controlador de clase parcial en las carpetas secundarias de la carpeta Platforms. Como alternativa, puedes configurar el proyecto para admitir la compatibilidad con múltiples versiones basada en nombre de archivo, basada en carpetas o ambas.

La aplicación de ejemplo está configurada para admitir la compatibilidad con múltiples versiones basada en nombre de archivo, de modo que las clases del controlador se encuentren en una sola carpeta:

Screenshot of the files in the Handlers folder of the project.

La clase VideoHandler que contiene los asignadores se denomina VideoHandler.cs. Sus implementaciones de plataforma se encuentran en los archivos VideoHandler.Android.cs, VideoHandler.MaciOS.cs y VideoHandler.Windows.cs. Esta compatibilidad con múltiples versiones basada en nombre de archivo se configura agregando el siguiente XML al archivo del proyecto, como elementos secundarios del nodo <Project>:

<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
  <Compile Remove="**\*.Android.cs" />
  <None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- iOS and Mac Catalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
  <Compile Remove="**\*.MaciOS.cs" />
  <None Include="**\*.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
  <Compile Remove="**\*.Windows.cs" />
  <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

Para obtener más información sobre cómo configurar la compatibilidad con múltiples versiones, consulta Configuración de compatibilidad con múltiples versiones.

Cada clase de controlador de plataforma debe ser una clase parcial y derivar de la clase genérica ViewHandler, que requiere dos argumentos de tipo:

  • La clase para el control multiplataforma, que se deriva de View.
  • El tipo de vista nativa que implementa el control multiplataforma en la plataforma. Debe ser idéntico al tipo de la propiedad PlatformView en el controlador.

Importante

La clase ViewHandler proporciona las propiedades VirtualView y PlatformView. La propiedad VirtualView se usa para acceder al control multiplataforma desde su controlador. La propiedad PlatformView se usa para acceder a la vista nativa en cada plataforma que implementa el control multiplataforma.

Cada una de las implementaciones del controlador de plataforma debe invalidar los métodos siguientes:

  • CreatePlatformView, que debe crear y devolver la vista nativa que implementa el control multiplataforma.
  • ConnectHandler, que debe realizar cualquier configuración de vista nativa, como inicializar la vista nativa y realizar suscripciones de eventos.
  • DisconnectHandler, que debe realizar cualquier limpieza de vista nativa, como anular la suscripción de eventos y eliminar objetos.

Importante

.NET MAUI no invoca intencionadamente el método DisconnectHandler. En su lugar, debes invocarlo tu mismo desde una ubicación adecuada en el ciclo de vida de la aplicación. Para más información, consulta Limpieza de vista nativa.

Cada controlador de plataforma también debe implementar las acciones definidas en los diccionarios del asignador.

Además, cada controlador de plataforma también debe proporcionar código, según sea necesario, para implementar la funcionalidad del control multiplataforma en la plataforma. Como alternativa, esto se puede proporcionar mediante un tipo adicional, que es el enfoque adoptado aquí.

Android

El vídeo se reproduce en Android con un objeto VideoView. Pero aquí, VideoView se ha encapsulado en un tipo MauiVideoPlayer para mantener la vista nativa separada de su controlador. En el ejemplo siguiente se muestra la clase parcial VideoHandler para Android, con sus tres invalidaciones:

#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;

namespace VideoDemos.Handlers
{
    public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
    {
        protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(Context, VirtualView);

        protected override void ConnectHandler(MauiVideoPlayer platformView)
        {
            base.ConnectHandler(platformView);

            // Perform any control setup here
        }

        protected override void DisconnectHandler(MauiVideoPlayer platformView)
        {
            platformView.Dispose();
            base.DisconnectHandler(platformView);
        }
        ...
    }
}

VideoHandler deriva de la clase ViewHandler, con el argumento genérico Video que especifica el tipo de control multiplataforma y el argumento MauiVideoPlayer que especifica el tipo que encapsula la vista nativa VideoView.

La invalidación CreatePlatformView crea y devuelve un objeto MauiVideoPlayer. La invalidación ConnectHandler es la ubicación para realizar cualquier configuración de vista nativa necesaria. La invalidación DisconnectHandler es la ubicación para realizar cualquier limpieza de vista nativa y, por tanto, llama al método Dispose en la instancia MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de propiedades:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateTransportControlsEnabled();
    }

    public static void MapSource(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateSource();
    }

    public static void MapIsLooping(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateIsLooping();
    }    

    public static void MapPosition(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdatePosition();
    }
    ...
}

Cada acción se ejecuta en respuesta a un cambio de propiedad en el control multiplataforma y es un método static que requiere instancias de controlador y de control multiplataforma como argumentos. En cada caso, la acción llama a un método definido en el tipo MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de comandos:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

Cada acción se ejecuta en respuesta a un comando que se envía desde el control multiplataforma y es un método static que requiere instancias de controlador y de control multiplataforma y datos opcionales como argumentos. En cada caso, la acción llama a un método definido en la clase MauiVideoPlayer, después de extraer los datos opcionales.

En Android, la clase MauiVideoPlayer encapsula VideoView para mantener la vista nativa separada de su controlador:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        MediaController _mediaController;
        bool _isPrepared;
        Context _context;
        Video _video;

        public MauiVideoPlayer(Context context, Video video) : base(context)
        {
            _context = context;
            _video = video;

            SetBackgroundColor(Color.Black);

            // Create a RelativeLayout for sizing the video
            RelativeLayout relativeLayout = new RelativeLayout(_context)
            {
                LayoutParameters = new CoordinatorLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
                {
                    Gravity = (int)GravityFlags.Center
                }
            };

            // Create a VideoView and position it in the RelativeLayout
            _videoView = new VideoView(context)
            {
                LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
            };

            // Add to the layouts
            relativeLayout.AddView(_videoView);
            AddView(relativeLayout);

            // Handle events
            _videoView.Prepared += OnVideoViewPrepared;
        }
        ...
    }
}

MauiVideoPlayer deriva de CoordinatorLayout, porque la vista nativa raíz de una aplicación .NET MAUI en Android es CoordinatorLayout. Aunque la clase MauiVideoPlayer podría derivar de otros tipos nativos de Android, puede ser difícil controlar el posicionamiento de la vista nativa en algunos escenarios.

VideoView se podría agregar directamente a CoordinatorLayout y colocarse en el diseño según sea necesario. Pero aquí, se agrega RelativeLayout de Android a CoordinatorLayout y VideoView se agrega a RelativeLayout. Los parámetros de diseño se establecen en RelativeLayout y VideoView para que se VideoView centre en la página y se expanda para rellenar el espacio disponible mientras mantiene su relación de aspecto.

El constructor también se suscribe al evento VideoView.Prepared. Este evento se genera cuando el vídeo está listo para su reproducción y se cancela en la invalidación Dispose:

public class MauiVideoPlayer : CoordinatorLayout
{
    VideoView _videoView;
    Video _video;
    ...

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _videoView.Prepared -= OnVideoViewPrepared;
            _videoView.Dispose();
            _videoView = null;
            _video = null;
        }

        base.Dispose(disposing);
    }
    ...
}

Además de cancelar la suscripción del evento Prepared, la invalidación Dispose también realiza la limpieza de la vista nativa.

Nota:

La invalidación DisconnectHandler del controlador llama a la invalidación Dispose.

Los controles de transporte de la plataforma incluyen botones que reproducen, pausan y detienen el vídeo, y los proporciona el tipo MediaController de Android. Si la propiedad Video.AreTransportControlsEnabled se establece en true, se establece MediaController como Media Player de VideoView. Esto ocurre porque cuando se establece la propiedad AreTransportControlsEnabled, el asignador de propiedades del controlador garantiza que se invoca el método MapAreTransportControlsEnabled, que a su vez llama al método UpdateTransportControlsEnabled en MauiVideoPlayer:

public class MauiVideoPlayer : CoordinatorLayout
{
    VideoView _videoView;
    MediaController _mediaController;
    Video _video;
    ...

    public void UpdateTransportControlsEnabled()
    {
        if (_video.AreTransportControlsEnabled)
        {
            _mediaController = new MediaController(_context);
            _mediaController.SetMediaPlayer(_videoView);
            _videoView.SetMediaController(_mediaController);
        }
        else
        {
            _videoView.SetMediaController(null);
            if (_mediaController != null)
            {
                _mediaController.SetMediaPlayer(null);
                _mediaController = null;
            }
        }
    }
    ...
}

Los controles de transporte se atenúan si no se usan, pero puedes pulsar en el vídeo para restaurarlos y verlos.

Si la propiedad Video.AreTransportControlsEnabled se establece en false, MediaController se quita como Media Player de VideoView. En este escenario, después, puedes controlar la reproducción de vídeo mediante programación, o bien puedes eliminar tus propios controles de transporte. Para más información, consulta Creación de los controles de transporte personalizados.

iOS y Mac Catalyst

El vídeo se reproduce en iOS y Mac Catalyst con AVPlayer y AVPlayerViewController. Pero aquí, estos tipos se encapsulan en un tipo MauiVideoPlayer para mantener las vistas nativas separadas de su controlador. En el ejemplo siguiente se muestra la clase parcial VideoHandler para iOS, con sus tres invalidaciones:

using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.MaciOS;

namespace VideoDemos.Handlers
{
    public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
    {
        protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);

        protected override void ConnectHandler(MauiVideoPlayer platformView)
        {
            base.ConnectHandler(platformView);

            // Perform any control setup here
        }

        protected override void DisconnectHandler(MauiVideoPlayer platformView)
        {
            platformView.Dispose();
            base.DisconnectHandler(platformView);
        }
        ...
    }
}

VideoHandler deriva de la clase ViewHandler, con el argumento genérico Video que especifica el tipo de control multiplataforma y el argumento MauiVideoPlayer que especifica el tipo que encapsula las vistas nativas AVPlayer y AVPlayerViewController.

La invalidación CreatePlatformView crea y devuelve un objeto MauiVideoPlayer. La invalidación ConnectHandler es la ubicación para realizar cualquier configuración de vista nativa necesaria. La invalidación DisconnectHandler es la ubicación para realizar cualquier limpieza de vista nativa y, por tanto, llama al método Dispose en la instancia MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de propiedades:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler?.PlatformView.UpdateTransportControlsEnabled();
    }

    public static void MapSource(VideoHandler handler, Video video)
    {
        handler?.PlatformView.UpdateSource();
    }

    public static void MapIsLooping(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateIsLooping();
    }    

    public static void MapPosition(VideoHandler handler, Video video)
    {
        handler?.PlatformView.UpdatePosition();
    }
    ...
}

Cada acción se ejecuta en respuesta a un cambio de propiedad en el control multiplataforma y es un método static que requiere instancias de controlador y de control multiplataforma como argumentos. En cada caso, la acción llama a un método definido en el tipo MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de comandos:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

Cada acción se ejecuta en respuesta a un comando que se envía desde el control multiplataforma, y es un método static que requiere instancias de controlmultiplataforma y controlador, y datos opcionales como argumentos. En cada caso, la acción llama a un método definido en la clase MauiVideoPlayer, después de extraer los datos opcionales.

En iOS y Mac Catalyst, la clase MauiVideoPlayer encapsula los tipos AVPlayer y AVPlayerViewController para mantener las vistas nativas separadas de su controlador:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        AVPlayerViewController _playerViewController;
        Video _video;
        ...

        public MauiVideoPlayer(Video video)
        {
            _video = video;

            _playerViewController = new AVPlayerViewController();
            _player = new AVPlayer();
            _playerViewController.Player = _player;
            _playerViewController.View.Frame = this.Bounds;

#if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER
            // On iOS 16 and Mac Catalyst 16, for Shell-based apps, the AVPlayerViewController has to be added to the parent ViewController, otherwise the transport controls won't be displayed.
            var viewController = WindowStateManager.Default.GetCurrentUIViewController();

            // If there's no view controller, assume it's not Shell and continue because the transport controls will still be displayed.
            if (viewController?.View is not null)
            {
                // Zero out the safe area insets of the AVPlayerViewController
                UIEdgeInsets insets = viewController.View.SafeAreaInsets;
                _playerViewController.AdditionalSafeAreaInsets = new UIEdgeInsets(insets.Top * -1, insets.Left, insets.Bottom * -1, insets.Right);

                // Add the View from the AVPlayerViewController to the parent ViewController
                viewController.View.AddSubview(_playerViewController.View);
            }
#endif
            // Use the View from the AVPlayerViewController as the native control
            AddSubview(_playerViewController.View);
        }
        ...
    }
}

MauiVideoPlayer deriva de UIView, que es la clase base en iOS y Mac Catalyst para objetos que muestran contenido y controlan la interacción del usuario con ese contenido. El constructor crea un objeto AVPlayer, que administra la reproducción y el tiempo de un archivo multimedia, y lo establece como el valor de propiedad Player de AVPlayerViewController. AVPlayerViewController muestra el contenido de AVPlayer y presenta controles de transporte y otras características. Después, se establece el tamaño y la ubicación del control, lo que garantiza que el vídeo se centre en la página y se expanda para rellenar el espacio disponible mientras mantiene su relación de aspecto. En iOS 16 y Mac Catalyst 16, AVPlayerViewController tiene que agregarse al elemento primario ViewController para las aplicaciones basadas en Shell; de lo contrario, no se muestran los controles de transporte. La vista nativa, que es la vista de AVPlayerViewController, se agrega a la página.

El método Dispose es responsable de realizar la limpieza de vista nativa:

public class MauiVideoPlayer : UIView
{
    AVPlayer _player;
    AVPlayerViewController _playerViewController;
    Video _video;
    ...

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_player != null)
            {
                DestroyPlayedToEndObserver();
                _player.ReplaceCurrentItemWithPlayerItem(null);
                _player.Dispose();
            }
            if (_playerViewController != null)
                _playerViewController.Dispose();

            _video = null;
        }

        base.Dispose(disposing);
    }
    ...
}

En algunos escenarios, los vídeos continúan reproduciéndose después de haber navegado por una página de reproducción de vídeo. Para detener el vídeo, ReplaceCurrentItemWithPlayerItem se establece en null en en la invalidación Dispose y se realiza otra limpieza de vista nativa.

Nota:

La invalidación DisconnectHandler del controlador llama a la invalidación Dispose.

Los controles de transporte de plataforma incluyen botones que reproducen, pausan y detienen el vídeo y los proporciona el tipo AVPlayerViewController. Si la propiedad Video.AreTransportControlsEnabled se establece en true, AVPlayerViewController mostrará sus controles de reproducción. Esto ocurre porque cuando se establece la propiedad AreTransportControlsEnabled, el asignador de propiedades del controlador garantiza que se invoca el método MapAreTransportControlsEnabled, que a su vez llama al método UpdateTransportControlsEnabled en MauiVideoPlayer:

public class MauiVideoPlayer : UIView
{
    AVPlayerViewController _playerViewController;
    Video _video;
    ...

    public void UpdateTransportControlsEnabled()
    {
        _playerViewController.ShowsPlaybackControls = _video.AreTransportControlsEnabled;
    }
    ...
}

Los controles de transporte se atenúan si no se usan, pero puedes pulsar en el vídeo para restaurarlos y verlos.

Si la propiedad Video.AreTransportControlsEnabled se establece en false, AVPlayerViewController no muestra sus controles de reproducción. En este escenario, puedes controlar la reproducción de vídeo mediante programación o dar tus propios controles de transporte. Para más información, consulta Creación de los controles de transporte personalizados.

Windows

El vídeo se reproduce en Windows con MediaPlayerElement. Pero, aquí, MediaPlayerElement se ha encapsulado en un tipo MauiVideoPlayer para mantener la vista nativa separada de su controlador. En el ejemplo siguiente se muestra la clase parcial VideoHandler de Windows, con sus tres invalidaciones:

#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Windows;

namespace VideoDemos.Handlers
{
    public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
    {
        protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);

        protected override void ConnectHandler(MauiVideoPlayer platformView)
        {
            base.ConnectHandler(platformView);

            // Perform any control setup here
        }

        protected override void DisconnectHandler(MauiVideoPlayer platformView)
        {
            platformView.Dispose();
            base.DisconnectHandler(platformView);
        }
        ...
    }
}

VideoHandler deriva de la clase ViewHandler, con el argumento Video genérico que especifica el tipo de control multiplataforma y el argumento MauiVideoPlayer que especifica el tipo que encapsula la vista nativa MediaPlayerElement.

La invalidación CreatePlatformView crea y devuelve un objeto MauiVideoPlayer. La invalidación ConnectHandler es la ubicación para realizar cualquier configuración de vista nativa necesaria. La invalidación DisconnectHandler es la ubicación para realizar cualquier limpieza de vista nativa y, por tanto, llama al método Dispose en la instancia MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de propiedades:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateTransportControlsEnabled();
    }

    public static void MapSource(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateSource();
    }

    public static void MapIsLooping(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateIsLooping();
    }

    public static void MapPosition(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdatePosition();
    }
    ...
}

Cada acción se ejecuta en respuesta a un cambio de propiedad en el control multiplataforma y es un método static que requiere instancias de controlador y de control multiplataforma como argumentos. En cada caso, la acción llama a un método definido en el tipo MauiVideoPlayer.

El controlador de plataforma también tiene que implementar las acciones definidas en el diccionario del asignador de comandos:

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

Cada acción se ejecuta en respuesta a un comando que se envía desde el control multiplataforma, y es un método static que requiere instancias de controlmultiplataforma y controlador, y datos opcionales como argumentos. En cada caso, la acción llama a un método definido en la clase MauiVideoPlayer, después de extraer los datos opcionales.

En Windows, la clase MauiVideoPlayer encapsula MediaPlayerElement para mantener la vista nativa separada de su controlador:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        ...

        public MauiVideoPlayer(Video video)
        {
            _video = video;
            _mediaPlayerElement = new MediaPlayerElement();
            this.Children.Add(_mediaPlayerElement);
        }
        ...
    }
}

MauiVideoPlayer deriva de Grid, y MediaPlayerElement se agrega como elemento secundario de Grid. Esto permite a MediaPlayerElement ajustar automáticamente el tamaño para rellenar todo el espacio disponible.

El método Dispose es responsable de realizar la limpieza de vista nativa:

public class MauiVideoPlayer : Grid, IDisposable
{
    MediaPlayerElement _mediaPlayerElement;
    Video _video;
    bool _isMediaPlayerAttached;
    ...

    public void Dispose()
    {
        if (_isMediaPlayerAttached)
        {
            _mediaPlayerElement.MediaPlayer.MediaOpened -= OnMediaPlayerMediaOpened;
            _mediaPlayerElement.MediaPlayer.Dispose();
        }
        _mediaPlayerElement = null;
    }
    ...
}

Además de anular la suscripción del evento MediaOpened, la invalidación Dispose también realiza la limpieza de la vista nativa.

Nota:

La invalidación DisconnectHandler del controlador llama a la invalidación Dispose.

Los controles de transporte de plataforma incluyen botones que reproducen, pausan y detienen el vídeo, y los facilita el tipo MediaPlayerElement. Si la propiedad Video.AreTransportControlsEnabled se establece en true, MediaPlayerElement mostrará sus controles de reproducción. Esto ocurre porque cuando se establece la propiedad AreTransportControlsEnabled, el asignador de propiedades del controlador garantiza que se invoca el método MapAreTransportControlsEnabled, que a su vez llama al método UpdateTransportControlsEnabled en MauiVideoPlayer:

public class MauiVideoPlayer : Grid, IDisposable
{
    MediaPlayerElement _mediaPlayerElement;
    Video _video;
    bool _isMediaPlayerAttached;
    ...

    public void UpdateTransportControlsEnabled()
    {
        _mediaPlayerElement.AreTransportControlsEnabled = _video.AreTransportControlsEnabled;
    }
    ...

}

Si la propiedad Video.AreTransportControlsEnabled se establece en false, MediaPlayerElement no muestra sus controles de reproducción. En este escenario, puedes controlar la reproducción de vídeo mediante programación o dar tus propios controles de transporte. Para más información, consulta Creación de los controles de transporte personalizados.

Conversión de un control multiplataforma en un control de plataforma

Cualquier control multiplataforma de .NET MAUI, que deriva de Element, se puede convertir a su control de plataforma subyacente con el método de extensión ToPlatform:

  • En Android, ToPlatform convierte un control de .NET MAUI en un objeto View de Android.
  • En iOS y Mac Catalyst, ToPlatform convierte un control de .NET MAUI en un objeto UIView.
  • En Windows, ToPlatform convierte un control de .NET MAUI en un objeto FrameworkElement.

Nota:

El método ToPlatform se encuentra en el espacio de nombres Microsoft.Maui.Platform.

En todas las plataformas, el método ToPlatform requiere un argumento MauiContext.

El método ToPlatform puede convertir un control multiplataforma en su control de plataforma subyacente desde el código de plataforma, como en una clase de controlador parcial para una plataforma:

using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;

namespace VideoDemos.Handlers
{
    public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
    {
        ...
        public static void MapSource(VideoHandler handler, Video video)
        {
            handler.PlatformView?.UpdateSource();

            // Convert cross-platform control to its underlying platform control
            MauiVideoPlayer mvp = (MauiVideoPlayer)video.ToPlatform(handler.MauiContext);
            ...
        }
        ...
    }
}

En este ejemplo, en la clase parcial VideoHandler para Android, el método MapSource convierte la instancia de Video en un objeto MauiVideoPlayer.

El método ToPlatform también puede convertir un control multiplataforma en su control de plataforma subyacente desde el código multiplataforma:

using Microsoft.Maui.Platform;

namespace VideoDemos.Views;

public partial class MyPage : ContentPage
{
    ...
    protected override void OnHandlerChanged()
    {
        // Convert cross-platform control to its underlying platform control
#if ANDROID
        Android.Views.View nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif IOS || MACCATALYST
        UIKit.UIView nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif WINDOWS
        Microsoft.UI.Xaml.FrameworkElement nativeView = video.ToPlatform(video.Handler.MauiContext);
#endif
        ...
    }
    ...
}

En este ejemplo, un control Video multiplataforma denominado video se convierte en su vista nativa subyacente en cada plataforma en la invalidación de OnHandlerChanged(). Se invoca esta invalidación cuando la vista nativa que implementa el control multiplataforma está disponible y se ha inicializado. El objeto devuelto por el método ToPlatform podría convertirse a su tipo nativo exacto, que aquí es MauiVideoPlayer.

Reproducción de un vídeo

La clase Video define una propiedad Source que se usa para especificar el origen del archivo de vídeo, así como una propiedad AutoPlay. AutoPlay adopta el valor predeterminado de true, lo que significa que el vídeo se debería comenzar a reproducir de forma automática después de establecer Source. Para ver la definición de estas propiedades, consulta Creación del control multiplataforma.

La propiedad Source es de tipo VideoSource, que es una clase abstracta que consta únicamente de tres métodos estáticos que crean instancias de las tres clases que derivan de VideoSource:

using System.ComponentModel;

namespace VideoDemos.Controls
{
    [TypeConverter(typeof(VideoSourceConverter))]
    public abstract class VideoSource : Element
    {
        public static VideoSource FromUri(string uri)
        {
            return new UriVideoSource { Uri = uri };
        }

        public static VideoSource FromFile(string file)
        {
            return new FileVideoSource { File = file };
        }

        public static VideoSource FromResource(string path)
        {
            return new ResourceVideoSource { Path = path };
        }
    }
}

La clase VideoSource incluye un atributo TypeConverter que hace referencia a VideoSourceConverter:

using System.ComponentModel;

namespace VideoDemos.Controls
{
    public class VideoSourceConverter : TypeConverter, IExtendedTypeConverter
    {
        object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                Uri uri;
                return Uri.TryCreate(value, UriKind.Absolute, out uri) && uri.Scheme != "file" ?
                    VideoSource.FromUri(value) : VideoSource.FromResource(value);
            }
            throw new InvalidOperationException("Cannot convert null or whitespace to VideoSource.");
        }
    }
}

Este convertidor de tipos se invoca cuando la propiedad Source se establece en una cadena en XAML. El método ConvertFromInvariantString intenta convertir la cadena en un objeto Uri. Si lo consigue, y el esquema no es file, el método devuelve UriVideoSource. De lo contrario, devuelve ResourceVideoSource.

Reproducción de un vídeo web

La clase UriVideoSource se usa para especificar un archivo de vídeo remoto con un identificador URI. Define una única propiedad Uri de tipo string:

namespace VideoDemos.Controls
{
    public class UriVideoSource : VideoSource
    {
        public static readonly BindableProperty UriProperty =
            BindableProperty.Create(nameof(Uri), typeof(string), typeof(UriVideoSource));

        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }
    }
}

Cuando la propiedad Source se establece en UriVideoSource, el asignador de propiedades del controlador garantiza que se invoque el método MapSource:

public static void MapSource(VideoHandler handler, Video video)
{
    handler?.PlatformView.UpdateSource();
}

El método MapSource a su vez llama al método UpdateSource en la propiedad PlatformView del controlador. La propiedad PlatformView, que es de tipo MauiVideoPlayer, representa la vista nativa que proporciona la implementación del reproductor de vídeo en cada plataforma.

Android

El vídeo se reproduce en Android con un objeto VideoView. En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo UriVideoSource:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        bool _isPrepared;
        Video _video;
        ...

        public void UpdateSource()
        {
            _isPrepared = false;
            bool hasSetSource = false;

            if (_video.Source is UriVideoSource)
            {
                string uri = (_video.Source as UriVideoSource).Uri;
                if (!string.IsNullOrWhiteSpace(uri))
                {
                    _videoView.SetVideoURI(Uri.Parse(uri));
                    hasSetSource = true;
                }
            }
            ...

            if (hasSetSource && _video.AutoPlay)
            {
                _videoView.Start();
            }
        }
        ...
    }
}

Al procesar objetos de tipo UriVideoSource, el método SetVideoUri de VideoView se usa para especificar el vídeo que se va a reproducir, con un objeto Uri de Android creado a partir del identificador URI de la cadena.

La propiedad AutoPlay no tiene equivalente en VideoView, por lo que se llama al método Start si se ha establecido un nuevo vídeo.

iOS y Mac Catalyst

Para reproducir un vídeo en iOS y Mac Catalyst, primero se crea un objeto de tipo AVAsset para encapsular el vídeo, que se usa para crear AVPlayerItem, que después se pasa al objeto AVPlayer. En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo UriVideoSource:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        AVPlayerItem _playerItem;
        Video _video;
        ...

        public void UpdateSource()
        {
            AVAsset asset = null;

            if (_video.Source is UriVideoSource)
            {
                string uri = (_video.Source as UriVideoSource).Uri;
                if (!string.IsNullOrWhiteSpace(uri))
                    asset = AVAsset.FromUrl(new NSUrl(uri));
            }
            ...

            if (asset != null)
                _playerItem = new AVPlayerItem(asset);
            else
                _playerItem = null;

            _player.ReplaceCurrentItemWithPlayerItem(_playerItem);
            if (_playerItem != null && _video.AutoPlay)
            {
                _player.Play();
            }
        }
        ...
    }
}

Al procesar objetos de tipo UriVideoSource, el método estático AVAsset.FromUrl se usa para especificar el vídeo que se va a reproducir, y se crea un objeto NSUrl de iOS a partir del identificador URI de la cadena.

La propiedad AutoPlay no cuenta con ningún equivalente en las clases de vídeo de iOS, por lo que la propiedad se examina al final del método UpdateSource para llamar al método Play en el objeto AVPlayer.

En algunos casos en iOS, los vídeos continúan reproduciéndose después de haber salido de la página de reproducción de vídeo. Para detener el vídeo, ReplaceCurrentItemWithPlayerItem se establece en null en la invalidación de Dispose:

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (_player != null)
        {
            _player.ReplaceCurrentItemWithPlayerItem(null);
            ...
        }
        ...
    }
    base.Dispose(disposing);
}

Windows

El vídeo se reproduce en Windows con MediaPlayerElement. En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo UriVideoSource:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        bool _isMediaPlayerAttached;
        ...

        public async void UpdateSource()
        {
            bool hasSetSource = false;

            if (_video.Source is UriVideoSource)
            {
                string uri = (_video.Source as UriVideoSource).Uri;
                if (!string.IsNullOrWhiteSpace(uri))
                {
                    _mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(uri));
                    hasSetSource = true;
                }
            }
            ...

            if (hasSetSource && !_isMediaPlayerAttached)
            {
                _isMediaPlayerAttached = true;
                _mediaPlayerElement.MediaPlayer.MediaOpened += OnMediaPlayerMediaOpened;
            }

            if (hasSetSource && _video.AutoPlay)
            {
                _mediaPlayerElement.AutoPlay = true;
            }
        }
        ...
    }
}

Al procesar objetos de tipo UriVideoSource, la propiedad MediaPlayerElement.Source se establece en un objeto MediaSource que inicializa un Uri con el identificador URI del vídeo que se va a reproducir. Cuando se ha establecido MediaPlayerElement.Source, el método del controlador de eventos OnMediaPlayerMediaOpened se registra para el evento MediaPlayerElement.MediaPlayer.MediaOpened. Este controlador de eventos se usa para establecer la propiedad Duration del control Video.

Al final del método UpdateSource, se examina la propiedad Video.AutoPlay, y si el valor es true, la propiedad MediaPlayerElement.AutoPlay se establece en true para iniciar la reproducción de vídeo.

Reproducir recurso de vídeo

La clase ResourceVideoSource se usa para acceder a los archivos de vídeo incrustados en la aplicación. Define una Pathpropiedad de tipostring:

namespace VideoDemos.Controls
{
    public class ResourceVideoSource : VideoSource
    {
        public static readonly BindableProperty PathProperty =
            BindableProperty.Create(nameof(Path), typeof(string), typeof(ResourceVideoSource));

        public string Path
        {
            get { return (string)GetValue(PathProperty); }
            set { SetValue(PathProperty, value); }
        }
    }
}

Cuando la propiedad Source se establece en ResourceVideoSource, el asignador de propiedades del controlador garantiza que se invoque el método MapSource:

public static void MapSource(VideoHandler handler, Video video)
{
    handler?.PlatformView.UpdateSource();
}

El método MapSource a su vez llama al método UpdateSource en la propiedad PlatformView del controlador. La propiedad PlatformView, que es de tipo MauiVideoPlayer, representa la vista nativa que proporciona la implementación del reproductor de vídeo en cada plataforma.

Android

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo ResourceVideoSource:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        bool _isPrepared;
        Context _context;
        Video _video;
        ...

        public void UpdateSource()
        {
            _isPrepared = false;
            bool hasSetSource = false;
            ...

            else if (_video.Source is ResourceVideoSource)
            {
                string package = Context.PackageName;
                string path = (_video.Source as ResourceVideoSource).Path;
                if (!string.IsNullOrWhiteSpace(path))
                {
                    string assetFilePath = "content://" + package + "/" + path;
                    _videoView.SetVideoPath(assetFilePath);
                    hasSetSource = true;
                }
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo ResourceVideoSource, el método SetVideoPath de VideoView se usa para especificar el vídeo que se va a reproducir, con un argumento de cadena que combina el nombre del paquete de la aplicación con el nombre de archivo del vídeo.

Un archivo de vídeo de recursos se almacena en la carpeta recursos del paquete y requiere que un proveedor de contenido acceda a él. La clase VideoProvider proporciona el proveedor de contenido, que crea un objeto AssetFileDescriptor que proporciona acceso al archivo de vídeo:

using Android.Content;
using Android.Content.Res;
using Android.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    [ContentProvider(new string[] { "com.companyname.videodemos" })]
    public class VideoProvider : ContentProvider
    {
        public override AssetFileDescriptor OpenAssetFile(Uri uri, string mode)
        {
            var assets = Context.Assets;
            string fileName = uri.LastPathSegment;
            if (fileName == null)
                throw new FileNotFoundException();

            AssetFileDescriptor afd = null;
            try
            {
                afd = assets.OpenFd(fileName);
            }
            catch (IOException ex)
            {
                Debug.WriteLine(ex);
            }
            return afd;
        }

        public override bool OnCreate()
        {
            return false;
        }
        ...
    }
}

iOS y Mac Catalyst

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo ResourceVideoSource:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        Video _video;
        ...

        public void UpdateSource()
        {
            AVAsset asset = null;
            ...

            else if (_video.Source is ResourceVideoSource)
            {
                string path = (_video.Source as ResourceVideoSource).Path;
                if (!string.IsNullOrWhiteSpace(path))
                {
                    string directory = Path.GetDirectoryName(path);
                    string filename = Path.GetFileNameWithoutExtension(path);
                    string extension = Path.GetExtension(path).Substring(1);
                    NSUrl url = NSBundle.MainBundle.GetUrlForResource(filename, extension, directory);
                    asset = AVAsset.FromUrl(url);
                }
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo ResourceVideoSource, el método GetUrlForResource de NSBundle se usa para recuperar el archivo del paquete de la aplicación. La ruta de acceso completa se debe dividir en un nombre de archivo, una extensión y un directorio.

En algunos casos en iOS, los vídeos continúan reproduciéndose después de haber salido de la página de reproducción de vídeo. Para detener el vídeo, ReplaceCurrentItemWithPlayerItem se establece en null en la invalidación de Dispose:

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (_player != null)
        {
            _player.ReplaceCurrentItemWithPlayerItem(null);
            ...
        }
        ...
    }
    base.Dispose(disposing);
}

Windows

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo ResourceVideoSource:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        ...

        public async void UpdateSource()
        {
            bool hasSetSource = false;

            ...
            else if (_video.Source is ResourceVideoSource)
            {
                string path = "ms-appx:///" + (_video.Source as ResourceVideoSource).Path;
                if (!string.IsNullOrWhiteSpace(path))
                {
                    _mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(path));
                    hasSetSource = true;
                }
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo ResourceVideoSource, la propiedad MediaPlayerElement.Source se establece en un objeto MediaSource que inicializa Uri con la ruta de acceso del recurso de vídeo prefijado con ms-appx:///.

Reproducir un archivo de vídeo desde la biblioteca del dispositivo

La clase FileVideoSource se usa para acceder a los vídeos desde la biblioteca de vídeos del dispositivo. Define una propiedad File de tipo string:

namespace VideoDemos.Controls
{
    public class FileVideoSource : VideoSource
    {
        public static readonly BindableProperty FileProperty =
            BindableProperty.Create(nameof(File), typeof(string), typeof(FileVideoSource));

        public string File
        {
            get { return (string)GetValue(FileProperty); }
            set { SetValue(FileProperty, value); }
        }
    }
}

Cuando la propiedad Source se establece en FileVideoSource, el asignador de propiedades del controlador garantiza que se invoque el método MapSource:

public static void MapSource(VideoHandler handler, Video video)
{
    handler?.PlatformView.UpdateSource();
}

El método MapSource a su vez llama al método UpdateSource en la propiedad PlatformView del controlador. La propiedad PlatformView, que es de tipo MauiVideoPlayer, representa la vista nativa que proporciona la implementación del reproductor de vídeo en cada plataforma.

Android

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo FileVideoSource:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        bool _isPrepared;
        Video _video;
        ...

        public void UpdateSource()
        {
            _isPrepared = false;
            bool hasSetSource = false;
            ...

            else if (_video.Source is FileVideoSource)
            {
                string filename = (_video.Source as FileVideoSource).File;
                if (!string.IsNullOrWhiteSpace(filename))
                {
                    _videoView.SetVideoPath(filename);
                    hasSetSource = true;
                }
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo FileVideoSource, el método SetVideoPath de VideoView se usa para especificar el archivo de vídeo que se va a reproducir.

iOS y Mac Catalyst

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo FileVideoSource:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        Video _video;
        ...

        public void UpdateSource()
        {
            AVAsset asset = null;
            ...

            else if (_video.Source is FileVideoSource)
            {
                string uri = (_video.Source as FileVideoSource).File;
                if (!string.IsNullOrWhiteSpace(uri))
                    asset = AVAsset.FromUrl(NSUrl.CreateFileUrl(new [] { uri }));
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo FileVideoSource, el método estático AVAsset.FromUrl se usa para especificar el archivo de vídeo que se va a reproducir, con el método NSUrl.CreateFileUrl que crea un objeto iOS NSUrl a partir del URI de cadena.

Windows

En el ejemplo de código siguiente se muestra cómo el método UpdateSource procesa la propiedad Source cuando es de tipo FileVideoSource:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        ...

        public async void UpdateSource()
        {
            bool hasSetSource = false;

            ...
            else if (_video.Source is FileVideoSource)
            {
                string filename = (_video.Source as FileVideoSource).File;
                if (!string.IsNullOrWhiteSpace(filename))
                {
                    StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename);
                    _mediaPlayerElement.Source = MediaSource.CreateFromStorageFile(storageFile);
                    hasSetSource = true;
                }
            }
            ...
        }
        ...
    }
}

Al procesar objetos de tipo FileVideoSource, el nombre de archivo de vídeo se convierte en un objeto StorageFile. Después, el método MediaSource.CreateFromStorageFile devuelve un objeto MediaSource que se establece como el valor de la propiedad MediaPlayerElement.Source.

Repetir en bucle un vídeo

La clase Video define una propiedad IsLooping, que permite que el control establezca automáticamente la posición del vídeo en el inicio después de alcanzar su final. El valor predeterminado es false, que indica que los vídeos no se repiten en bucle automáticamente.

Cuando se establece la propiedad IsLooping, el asignador de propiedades del controlador garantiza que se invoca el método MapIsLooping:

public static void MapIsLooping(VideoHandler handler, Video video)
{
    handler.PlatformView?.UpdateIsLooping();
}  

A su vez, el método MapIsLooping llama al método UpdateIsLooping en la propiedad PlatformView del controlador. La propiedad PlatformView, que es de tipo MauiVideoPlayer, representa la vista nativa que proporciona la implementación del reproductor de vídeo en cada plataforma.

Android

En el ejemplo de código siguiente se muestra cómo el método UpdateIsLooping en Android habilita el bucle de vídeo:

using Android.Content;
using Android.Media;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout, MediaPlayer.IOnPreparedListener
    {
        VideoView _videoView;
        Video _video;
        ...

        public void UpdateIsLooping()
        {
            if (_video.IsLooping)
            {
                _videoView.SetOnPreparedListener(this);
            }
            else
            {
                _videoView.SetOnPreparedListener(null);
            }
        }

        public void OnPrepared(MediaPlayer mp)
        {
            mp.Looping = _video.IsLooping;
        }
        ...
    }
}

Para habilitar el bucle de vídeo, la clase MauiVideoPlayer implementa la interfaz MediaPlayer.IOnPreparedListener. Esta interfaz define una devolución de llamada OnPrepared que se invoca cuando el origen multimedia está listo para la reproducción. Cuando la propiedad Video.IsLooping es true, el método UpdateIsLooping establece MauiVideoPlayer como el objeto que proporciona la devolución de llamada OnPrepared. La devolución de llamada establece la propiedad MediaPlayer.IsLooping en el valor de la propiedad Video.IsLooping.

iOS y Mac Catalyst

En el ejemplo de código siguiente se muestra cómo el método UpdateIsLooping en iOS y Mac Catalyst habilita el bucle de vídeo:

using System.Diagnostics;
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        AVPlayerViewController _playerViewController;
        Video _video;
        NSObject? _playedToEndObserver;
        ...

        public void UpdateIsLooping()
        {
            DestroyPlayedToEndObserver();
            if (_video.IsLooping)
            {
                _player.ActionAtItemEnd = AVPlayerActionAtItemEnd.None;
                _playedToEndObserver = NSNotificationCenter.DefaultCenter.AddObserver(AVPlayerItem.DidPlayToEndTimeNotification, PlayedToEnd);
            }
            else
                _player.ActionAtItemEnd = AVPlayerActionAtItemEnd.Pause;
        }

        void PlayedToEnd(NSNotification notification)
        {
            if (_video == null || notification.Object != _playerViewController.Player?.CurrentItem)
                return;

            _playerViewController.Player?.Seek(CMTime.Zero);
        }
        ...
    }
}

En iOS y Mac Catalyst, se usa una notificación para ejecutar una devolución de llamada cuando el vídeo se ha reproducido hasta el final. Cuando la propiedad Video.IsLooping es true, el método UpdateIsLooping agrega un observador para la notificación AVPlayerItem.DidPlayToEndTimeNotification y ejecuta el método PlayedToEnd cuando se recibe la notificación. A su vez, este método reanuda la reproducción desde el principio del vídeo. Si la propiedad Video.IsLooping es false, el vídeo se pausa al final de la reproducción.

Dado que MauiVideoPlayer agrega un observador para una notificación, también debe quitar el observador al realizar la limpieza de vista nativa. Esto se logra en la invalidación Dispose:

public class MauiVideoPlayer : UIView
{
    AVPlayer _player;
    AVPlayerViewController _playerViewController;
    Video _video;
    NSObject? _playedToEndObserver;
    ...

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_player != null)
            {
                DestroyPlayedToEndObserver();
                ...
            }
            ...
        }

        base.Dispose(disposing);
    }

    void DestroyPlayedToEndObserver()
    {
        if (_playedToEndObserver != null)
        {
            NSNotificationCenter.DefaultCenter.RemoveObserver(_playedToEndObserver);
            DisposeObserver(ref _playedToEndObserver);
        }
    }

    void DisposeObserver(ref NSObject? disposable)
    {
        disposable?.Dispose();
        disposable = null;
    }
    ...
}

La invalidación Dispose llama al método DestroyPlayedToEndObserver que quita el observador de la notificación AVPlayerItem.DidPlayToEndTimeNotification, y que también invoca el método Dispose en NSObject.

Windows

En el ejemplo de código siguiente se muestra cómo el método UpdateIsLooping en Windows habilita el bucle de vídeo:

public void UpdateIsLooping()
{
    if (_isMediaPlayerAttached)
        _mediaPlayerElement.MediaPlayer.IsLoopingEnabled = _video.IsLooping;
}

Para habilitar el bucle de vídeo, el método UpdateIsLooping establece la propiedad MediaPlayerElement.MediaPlayer.IsLoopingEnabled en el valor de la propiedad Video.IsLooping.

Creación de controles de transporte personalizados

Los controles de transporte de un reproductor de vídeo incluyen los botones que reproducen, pausan y detienen el vídeo. Estos botones suelen identificarse con iconos conocidos en lugar de texto, y las funciones de reproducción y pausa suelen combinarse en un mismo botón.

De forma predeterminada, el control Video muestra controles de transporte compatibles con cada plataforma. Pero, al establecer la propiedad AreTransportControlsEnabled en false, se eliminan estos controles. Después, puedes controlar la reproducción de vídeo mediante programación, o bien, puedes facilitar tus propios controles de transporte.

Implementar tus propios controles de transporte requiere que la clase Video pueda notificar a sus vistas nativas para reproducir, pausar o detener el vídeo y conocer el estado actual de la reproducción de vídeo. La clase Video define los métodos denominados Play, Pause y Stop que generan un evento correspondiente y envían un comando a VideoHandler:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        public event EventHandler<VideoPositionEventArgs> PlayRequested;
        public event EventHandler<VideoPositionEventArgs> PauseRequested;
        public event EventHandler<VideoPositionEventArgs> StopRequested;

        public void Play()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            PlayRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.PlayRequested), args);
        }

        public void Pause()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            PauseRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.PauseRequested), args);
        }

        public void Stop()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            StopRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.StopRequested), args);
        }
    }
}

La clase VideoPositionEventArgs define una propiedad Position que se puede establecer a través de su constructor. Esta propiedad representa la posición en la que se inició, pausó o detuvo la reproducción de vídeo.

La línea final de los métodos Play, Pause y Stop envía un comando y los datos asociados a VideoHandler. CommandMapper para VideoHandler asigna nombres de comando a acciones que se ejecutan cuando se recibe un comando. Por ejemplo, cuando VideoHandler recibe el comando PlayRequested, ejecuta su método MapPlayRequested. La ventaja de este enfoque es que elimina la necesidad de que las vistas nativas se suscriban a eventos de control multiplataforma y cancelen la suscripción a estos. Además, permite una personalización sencilla, ya que el asignador de comandos se puede modificar mediante consumidores de control multiplataforma sin subclases. Para obtener más información sobre CommandMapper, consulta Creación del asignador de comandos.

La implementación MauiVideoPlayer en Android, iOS y Mac Catalyst, tiene métodos PlayRequested, PauseRequested y StopRequested que se ejecutan en respuesta al control Video que envía comandos PlayRequested, PauseRequested y StopRequested . Cada método invoca un método en su vista nativa para reproducir, pausar o detener el vídeo. Por ejemplo, el código siguiente muestra los métodos PlayRequested, PauseRequested y StopRequested en iOS y Mac Catalyst:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        ...

        public void PlayRequested(TimeSpan position)
        {
            _player.Play();
            Debug.WriteLine($"Video playback from {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
        }

        public void PauseRequested(TimeSpan position)
        {
            _player.Pause();
            Debug.WriteLine($"Video paused at {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
        }

        public void StopRequested(TimeSpan position)
        {
            _player.Pause();
            _player.Seek(new CMTime(0, 1));
            Debug.WriteLine($"Video stopped at {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
        }
    }
}

Cada uno de los tres métodos registra la posición en la que se reprodujo, pausó o detuvo el vídeo mediante los datos que se envían con el comando.

Este mecanismo garantiza que cuando se invoca el método Play, Pause o Stop en el control Video, se indica a su vista nativa que reproduzca, pause o detenga el vídeo y registre la posición en la que se reprodujo, pausó o detuvo el vídeo. Todo esto sucede con un método desacoplado, sin que las vistas nativas tengan que suscribirse a eventos multiplataforma.

Estado del vídeo

Implementar las funciones de reproducción, pausa y parada no es suficiente para admitir los controles de transporte personalizados. Con frecuencia, los comandos de reproducción y pausa deben implementarse con el mismo botón, que cambia su apariencia para indicar si el vídeo está reproduciéndose o en pausa en ese momento. Además, el botón no debe habilitarse si el vídeo aún no se ha cargado.

Estos requisitos implican que el reproductor de vídeo necesita mostrar un estado actual que indique si está reproduciéndose o en pausa, o bien si aún no está preparado para reproducir un vídeo. Este estado se puede representar mediante una enumeración:

public enum VideoStatus
{
    NotReady,
    Playing,
    Paused
}

La clase Video define una propiedad enlazable de solo lectura denominada Status del tipo VideoStatus. Esta propiedad se define como de solo lectura porque únicamente tiene que establecerse desde el controlador de la plataforma:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        private static readonly BindablePropertyKey StatusPropertyKey =
            BindableProperty.CreateReadOnly(nameof(Status), typeof(VideoStatus), typeof(Video), VideoStatus.NotReady);

        public static readonly BindableProperty StatusProperty = StatusPropertyKey.BindableProperty;

        public VideoStatus Status
        {
            get { return (VideoStatus)GetValue(StatusProperty); }
        }

        VideoStatus IVideoController.Status
        {
            get { return Status; }
            set { SetValue(StatusPropertyKey, value); }
        }
        ...
    }
}

Normalmente, una propiedad enlazable de solo lectura tendría un descriptor de acceso set privado en la propiedad Status para permitirle establecerlo en la clase. Pero, para un elemento derivado View admitido por controladores, la propiedad debe establecerse desde fuera de la clase, pero solo por el controlador del control.

Por este motivo, se define otra propiedad con el nombre IVideoController.Status. Esta es una implementación de interfaz explícita y es posible mediante la interfaz IVideoController que implementa la clase Video:

public interface IVideoController
{
    VideoStatus Status { get; set; }
    TimeSpan Duration { get; set; }
}

Esta interfaz permite que una clase externa a Video pueda establecer la propiedad Status al hacer referencia a la interfaz IVideoController. La propiedad también se puede establecer desde otras clases y el controlador, pero es poco probable que se establezca por error. Aún más importante, la propiedad Status no se puede establecer mediante un enlace de datos.

Para ayudar a las implementaciones del controlador a mantener actualizada la propiedad Status, la clase Video define un evento UpdateStatus y un comando:

using System.ComponentModel;

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        public event EventHandler UpdateStatus;

        IDispatcherTimer _timer;

        public Video()
        {
            _timer = Dispatcher.CreateTimer();
            _timer.Interval = TimeSpan.FromMilliseconds(100);
            _timer.Tick += OnTimerTick;
            _timer.Start();
        }

        ~Video() => _timer.Tick -= OnTimerTick;

        void OnTimerTick(object sender, EventArgs e)
        {
            UpdateStatus?.Invoke(this, EventArgs.Empty);
            Handler?.Invoke(nameof(Video.UpdateStatus));
        }
        ...
    }
}

El controlador de eventos OnTimerTick se ejecuta cada décima de segundo, lo que genera el evento UpdateStatus e invoca el comando UpdateStatus.

Cuando el comando UpdateStatus se envía desde el control Video a su controlador, el asignador de comandos del controlador garantiza que se invoca el método MapUpdateStatus:

public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
    handler.PlatformView?.UpdateStatus();
}

El método MapUpdateStatus a su vez llama al método UpdateStatus en la propiedad PlatformView del controlador. La propiedad PlatformView, que es de tipo MauiVideoPlayer, encapsula las vistas nativas que proporcionan la implementación del reproductor de vídeo en cada plataforma.

Android

En el ejemplo de código siguiente muestra que el método UpdateStatus en Android establece la propiedad Status:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        bool _isPrepared;
        Video _video;
        ...

        public MauiVideoPlayer(Context context, Video video) : base(context)
        {
            _video = video;
            ...
            _videoView.Prepared += OnVideoViewPrepared;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _videoView.Prepared -= OnVideoViewPrepared;
                ...
            }

            base.Dispose(disposing);
        }

        void OnVideoViewPrepared(object sender, EventArgs args)
        {
            _isPrepared = true;
            ((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
        }

        public void UpdateStatus()
        {
            VideoStatus status = VideoStatus.NotReady;

            if (_isPrepared)
                status = _videoView.IsPlaying ? VideoStatus.Playing : VideoStatus.Paused;

            ((IVideoController)_video).Status = status;
            ...
        }
        ...
    }
}

La propiedad VideoView.IsPlaying es un operador booleano que solo indica si el vídeo está reproduciéndose o está en pausa. Para determinar si el elemento VideoView aún no puede reproducir ni pausar el vídeo, es necesario controlar su evento Prepared. Este evento se genera cuando el origen multimedia está listo para la reproducción. El evento se suscribe en el constructor MauiVideoPlayer y se cancela la suscripción de su invalidación Dispose. Luego el método UpdateStatus usa el campo isPrepared y la propiedad VideoView.IsPlaying para establecer la propiedad Status en el objeto Video convirtiéndola a IVideoController.

iOS y Mac Catalyst

En el ejemplo de código siguiente se muestra el método UpdateStatus en iOS y Mac Catalyst establece la propiedad Status:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        Video _video;
        ...

        public void UpdateStatus()
        {
            VideoStatus videoStatus = VideoStatus.NotReady;

            switch (_player.Status)
            {
                case AVPlayerStatus.ReadyToPlay:
                    switch (_player.TimeControlStatus)
                    {
                        case AVPlayerTimeControlStatus.Playing:
                            videoStatus = VideoStatus.Playing;
                            break;

                        case AVPlayerTimeControlStatus.Paused:
                            videoStatus = VideoStatus.Paused;
                            break;
                    }
                    break;
            }
            ((IVideoController)_video).Status = videoStatus;
            ...
        }
        ...
    }
}

Se debe tener acceso a dos propiedades de AVPlayer para establecer la propiedad Status: la propiedad Status de tipo AVPlayerStatus y la propiedad TimeControlStatus de tipo AVPlayerTimeControlStatus. Después, la propiedad Status se puede establecer en el objeto Video convirtiéndola a IVideoController.

Windows

En el ejemplo de código siguiente se muestra el método UpdateStatus en Windows que establece la propiedad Status:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        bool _isMediaPlayerAttached;
        ...

        public void UpdateStatus()
        {
            if (_isMediaPlayerAttached)
            {
                VideoStatus status = VideoStatus.NotReady;

                switch (_mediaPlayerElement.MediaPlayer.CurrentState)
                {
                    case MediaPlayerState.Playing:
                        status = VideoStatus.Playing;
                        break;
                    case MediaPlayerState.Paused:
                    case MediaPlayerState.Stopped:
                        status = VideoStatus.Paused;
                        break;
                }

                ((IVideoController)_video).Status = status;
                _video.Position = _mediaPlayerElement.MediaPlayer.Position;
            }
        }
        ...
    }
}

El método UpdateStatus usa el valor devuelto por MediaPlayerElement.MediaPlayer.CurrentState para determinar el origen del valor de propiedad Status. Después, la propiedad Status se puede establecer en el objeto Video convirtiéndola a IVideoController.

Barra de posicionamiento

Los controles de transporte que cada plataforma implementa incluyen una barra de posicionamiento. Esta barra es similar a un control deslizante o una barra de desplazamiento y muestra la ubicación actual del vídeo dentro de su duración total. Los usuarios pueden manipular la barra de posicionamiento para avanzar o retroceder a una nueva posición en el vídeo.

La implementación de su propia barra de posicionamiento requiere que la clase Video conozca la duración del vídeo y su posición actual dentro de esa duración.

Duration

Un elemento de información que el control Video necesita para admitir una barra de posicionamiento personalizada es la duración del vídeo. La clase Video define una propiedad enlazable de solo lectura denominada Duration del tipo TimeSpan. Esta propiedad se define como de solo lectura porque únicamente tiene que establecerse desde el controlador de la plataforma:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        private static readonly BindablePropertyKey DurationPropertyKey =
            BindableProperty.CreateReadOnly(nameof(Duration), typeof(TimeSpan), typeof(Video), new TimeSpan(),
                propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());

        public static readonly BindableProperty DurationProperty = DurationPropertyKey.BindableProperty;

        public TimeSpan Duration
        {
            get { return (TimeSpan)GetValue(DurationProperty); }
        }

        TimeSpan IVideoController.Duration
        {
            get { return Duration; }
            set { SetValue(DurationPropertyKey, value); }
        }
        ...
    }
}

Normalmente, una propiedad enlazable de solo lectura tendría un descriptor de acceso set privado en la propiedad Duration para permitirle establecerlo en la clase. Pero, para un elemento derivado View admitido por controladores, la propiedad debe establecerse desde fuera de la clase, pero solo por el controlador del control.

Nota:

El controlador de eventos de cambio de propiedad para la propiedad enlazable Duration llama a un método denominado SetTimeToEnd, que se describe en Cálculo del tiempo de finalización.

Por este motivo, se define otra propiedad con el nombre IVideoController.Duration. Esta es una implementación de interfaz explícita y es posible mediante la interfaz IVideoController que implementa la clase Video:

public interface IVideoController
{
    VideoStatus Status { get; set; }
    TimeSpan Duration { get; set; }
}

Esta interfaz permite que una clase externa a Video establezca la propiedad Duration haciendo referencia a la interfaz IVideoController. La propiedad también se puede establecer desde otras clases y el controlador, pero es poco probable que se establezca por error. Aún más importante, la propiedad Duration no se puede establecer mediante un enlace de datos.

La duración de un vídeo no está disponible inmediatamente después de que se establezca la propiedad Source del control Video. El archivo de vídeo debe descargarse parcialmente antes de que el reproductor de vídeo subyacente pueda determinar su duración.

Android

En Android, la propiedad VideoView.Duration informa de una duración válida en milisegundos después de que se haya generado el evento VideoView.Prepared. La clase MauiVideoPlayer usa el controlador de eventos Prepared para obtener el valor de propiedad Duration:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        Video _video;
        ...

        void OnVideoViewPrepared(object sender, EventArgs args)
        {
            ...
            ((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
        }
        ...
    }
}
iOS y Mac Catalyst

En iOS y Mac Catalyst, la duración de un vídeo se obtiene a partir de la propiedad AVPlayerItem.Duration, pero no inmediatamente después de que se haya creado AVPlayerItem. Es posible establecer un observador de iOS para la propiedad Duration, pero la clase MauiVideoPlayer obtiene la duración en el método UpdateStatus, al que se llama 10 veces por segundo:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayerItem _playerItem;
        ...

        TimeSpan ConvertTime(CMTime cmTime)
        {
            return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
        }

        public void UpdateStatus()
        {
            ...
            if (_playerItem != null)
            {
                ((IVideoController)_video).Duration = ConvertTime(_playerItem.Duration);
                ...
            }
        }
        ...
    }
}

El método ConvertTime convierte un objeto CMTime en un valor TimeSpan.

Windows

En Windows, la propiedad MediaPlayerElement.MediaPlayer.NaturalDuration es un valor TimeSpan que se convierte en válido cuando se ha generado el evento MediaPlayerElement.MediaPlayer.MediaOpened. La clase MauiVideoPlayer usa el controlador de eventos MediaOpened para obtener el valor de propiedad NaturalDuration:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        bool _isMediaPlayerAttached;
        ...

        void OnMediaPlayerMediaOpened(MediaPlayer sender, object args)
        {
            MainThread.BeginInvokeOnMainThread(() =>
            {
                ((IVideoController)_video).Duration = _mediaPlayerElement.MediaPlayer.NaturalDuration;
            });
        }
        ...
    }
}

Después, el controlador de eventos OnMediaPlayer llama al método MainThread.BeginInvokeOnMainThread para establecer la propiedad Duration en el objeto Video, convirtiéndolo a IVideoController en el subproceso principal. Esto es necesario porque el evento MediaPlayerElement.MediaPlayer.MediaOpened se controla en un subproceso en segundo plano. Para más información sobre cómo ejecutar código en ejecución, consulta Creación de un subproceso en el subproceso de .NET MAUI UI.

Posición

Video también necesita una propiedad Position que aumente de cero a Duration mientras se reproduce el vídeo. La clase Video implementa esta propiedad como una propiedad enlazable con descriptores de acceso get y set públicos:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        public static readonly BindableProperty PositionProperty =
            BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(Video), new TimeSpan(),
                propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());

        public TimeSpan Position
        {
            get { return (TimeSpan)GetValue(PositionProperty); }
            set { SetValue(PositionProperty, value); }
        }
        ...
    }
}

El descriptor de acceso get devuelve la posición actual del vídeo como reproducción. El descriptor de acceso set responde a la manipulación del usuario de la barra de posicionamiento moviendo la posición del vídeo hacia delante o hacia atrás.

Nota:

El controlador de eventos modificado por propiedades para la propiedad enlazable Position llama a un método denominado SetTimeToEnd, que se describe en Cálculo del tiempo de finalización.

En Android, iOS y Mac Catalyst, la propiedad que obtiene la posición actual solo tiene un descriptor de acceso get. En su lugar, hay un método Seek disponible para establecer la posición. Esto parece ser un método más razonable que usar una sola propiedad Position, que tiene un problema inherente. Mientras se reproduce un vídeo, la propiedad Position debe actualizarse continuamente para reflejar la nueva posición. Pero no es recomendable que la mayoría de los cambios en la propiedad Position hagan que el reproductor de vídeo se mueva a una nueva posición en el vídeo. Si eso ocurriera, el reproductor de vídeo, como respuesta, buscaría el último valor de la propiedad Position y el vídeo no avanzaría.

A pesar de las dificultades de implementar una propiedad Position con descriptores de acceso get y set, este método se usa porque puede usar el enlace de datos. La propiedad Position del control Video puede enlazarse a un Slider que se usa para mostrar la posición y para buscar una nueva posición. Pero se deben tomar varias precauciones al implementar la propiedad Position para evitar bucles de retroalimentación.

Android

En Android, la propiedad VideoView.CurrentPosition indica la posición actual del vídeo. La clase MauiVideoPlayer establece la propiedad Position en el método UpdateStatus al mismo tiempo que establece la propiedad Duration:

using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;

namespace VideoDemos.Platforms.Android
{
    public class MauiVideoPlayer : CoordinatorLayout
    {
        VideoView _videoView;
        Video _video;
        ...

        public void UpdateStatus()
        {
            ...
            TimeSpan timeSpan = TimeSpan.FromMilliseconds(_videoView.CurrentPosition);
            _video.Position = timeSpan;
        }

        public void UpdatePosition()
        {
            if (Math.Abs(_videoView.CurrentPosition - _video.Position.TotalMilliseconds) > 1000)
            {
                _videoView.SeekTo((int)_video.Position.TotalMilliseconds);
            }
        }
        ...
    }
}

Cada vez que el método UpdateStatus establece la propiedad Position, la propiedad Position desencadena un evento PropertyChanged, lo que hace que el asignador de propiedades para el controlador llame al método UpdatePosition. El método UpdatePosition no debe hacer nada para la mayoría de los cambios de propiedad. En caso contrario, con cada cambio en la posición del vídeo, se movería a la misma posición a la que ha llegado. Para evitar este bucle de retroalimentación, UpdatePosition solo llama al método Seek en el objeto VideoView cuando la diferencia entre la propiedad Position y la posición actual del VideoView es mayor que un segundo.

iOS y Mac Catalyst

En iOS y Mac Catalyst, la propiedad AVPlayerItem.CurrentTime indica la posición actual del vídeo. La clase MauiVideoPlayer establece la propiedad Position en el método UpdateStatus al mismo tiempo que establece la propiedad Duration:

using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;

namespace VideoDemos.Platforms.MaciOS
{
    public class MauiVideoPlayer : UIView
    {
        AVPlayer _player;
        AVPlayerItem _playerItem;
        Video _video;
        ...

        TimeSpan ConvertTime(CMTime cmTime)
        {
            return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
        }

        public void UpdateStatus()
        {
            ...
            if (_playerItem != null)
            {
                ...
                _video.Position = ConvertTime(_playerItem.CurrentTime);
            }
        }

        public void UpdatePosition()
        {
            TimeSpan controlPosition = ConvertTime(_player.CurrentTime);
            if (Math.Abs((controlPosition - _video.Position).TotalSeconds) > 1)
            {
                _player.Seek(CMTime.FromSeconds(_video.Position.TotalSeconds, 1));
            }
        }
        ...
    }
}

Cada vez que el método UpdateStatus establece la propiedad Position, la propiedad Position desencadena un evento PropertyChanged, lo que hace que el asignador de propiedades para el controlador llame al método UpdatePosition. El método UpdatePosition no debe hacer nada para la mayoría de los cambios de propiedad. En caso contrario, con cada cambio en la posición del vídeo, se movería a la misma posición a la que ha llegado. Para evitar este bucle de retroalimentación, UpdatePosition solo llama al método Seek en el objeto AVPlayer cuando la diferencia entre la propiedad Position y la posición actual del AVPlayer es mayor que un segundo.

Windows

En Windows, la propiedad MediaPlayerElement.MedaPlayer.Position indica la posición actual del vídeo. La clase MauiVideoPlayer establece la propiedad Position en el método UpdateStatus al mismo tiempo que establece la propiedad Duration:

using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;

namespace VideoDemos.Platforms.Windows
{
    public class MauiVideoPlayer : Grid, IDisposable
    {
        MediaPlayerElement _mediaPlayerElement;
        Video _video;
        bool _isMediaPlayerAttached;
        ...

        public void UpdateStatus()
        {
            if (_isMediaPlayerAttached)
            {
                ...
                _video.Position = _mediaPlayerElement.MediaPlayer.Position;
            }
        }

        public void UpdatePosition()
        {
            if (_isMediaPlayerAttached)
            {
                if (Math.Abs((_mediaPlayerElement.MediaPlayer.Position - _video.Position).TotalSeconds) > 1)
                {
                    _mediaPlayerElement.MediaPlayer.Position = _video.Position;
                }
            }
        }
        ...
    }
}

Cada vez que el método UpdateStatus establece la propiedad Position, la propiedad Position desencadena un evento PropertyChanged, lo que hace que el asignador de propiedades para el controlador llame al método UpdatePosition. El método UpdatePosition no debe hacer nada para la mayoría de los cambios de propiedad. En caso contrario, con cada cambio en la posición del vídeo, se movería a la misma posición a la que ha llegado. Para evitar este bucle de retroalimentación, UpdatePosition solo establece la propiedad MediaPlayerElement.MediaPlayer.Position cuando la diferencia entre la propiedad Position y la posición actual del MediaPlayerElement es mayor que un segundo.

Cálculo del tiempo de finalización

A veces, los reproductores de vídeo muestran el tiempo restante en el vídeo. Este valor comienza en la duración del vídeo cuando el vídeo empieza y disminuye a cero cuando el vídeo finaliza.

La clase Video incluye una propiedad TimeToEnd de solo lectura que se calcula en función de los cambios realizados en las propiedades Duration y Position:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        private static readonly BindablePropertyKey TimeToEndPropertyKey =
            BindableProperty.CreateReadOnly(nameof(TimeToEnd), typeof(TimeSpan), typeof(Video), new TimeSpan());

        public static readonly BindableProperty TimeToEndProperty = TimeToEndPropertyKey.BindableProperty;

        public TimeSpan TimeToEnd
        {
            get { return (TimeSpan)GetValue(TimeToEndProperty); }
            private set { SetValue(TimeToEndPropertyKey, value); }
        }

        void SetTimeToEnd()
        {
            TimeToEnd = Duration - Position;
        }
        ...
    }
}

El método SetTimeToEnd se llama desde los controladores de eventos de cambio de propiedad de las propiedades Duration y Position.

Barra de posicionamiento personalizada

Una barra de posicionamiento personalizada se puede implementar mediante la creación de una clase que deriva de Slider, que contiene propiedades Duration y Position de tipo TimeSpan:

namespace VideoDemos.Controls
{
    public class PositionSlider : Slider
    {
        public static readonly BindableProperty DurationProperty =
            BindableProperty.Create(nameof(Duration), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(1),
                propertyChanged: (bindable, oldValue, newValue) =>
                {
                    double seconds = ((TimeSpan)newValue).TotalSeconds;
                    ((Slider)bindable).Maximum = seconds <= 0 ? 1 : seconds;
                });

        public static readonly BindableProperty PositionProperty =
            BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(0),
                defaultBindingMode: BindingMode.TwoWay,
                propertyChanged: (bindable, oldValue, newValue) =>
                {
                    double seconds = ((TimeSpan)newValue).TotalSeconds;
                    ((Slider)bindable).Value = seconds;
                });

        public TimeSpan Duration
        {
            get { return (TimeSpan)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }

        public TimeSpan Position
        {
            get { return (TimeSpan)GetValue(PositionProperty); }
            set { SetValue (PositionProperty, value); }
        }

        public PositionSlider()
        {
            PropertyChanged += (sender, args) =>
            {
                if (args.PropertyName == "Value")
                {
                    TimeSpan newPosition = TimeSpan.FromSeconds(Value);
                    if (Math.Abs(newPosition.TotalSeconds - Position.TotalSeconds) / Duration.TotalSeconds > 0.01)
                        Position = newPosition;
                }
            };
        }
    }
}

El controlador de eventos de cambio de propiedad para la propiedad Duration establece la propiedad Maximum del Slider a la propiedad TotalSeconds del valor TimeSpan. De forma similar, el controlador de eventos de cambio de propiedad para Position establece la propiedad Value del Slider. Este es el mecanismo por el que Slider realiza un seguimiento de la posición de PositionSlider.

PositionSlider se actualiza desde el Slider subyacente en un único escenario, que es cuando el usuario manipula el Slider para indicar que el vídeo debe avanzar o retroceder a una posición nueva. Esto se detecta en el controlador PropertyChanged en el constructor PositionSlider. El controlador de eventos comprueba si hay algún cambio en la propiedad Value y, si es diferente de la propiedad Position, la propiedad Position se establece desde la propiedad Value.

Registro del controlador

Un control personalizado y su controlador deben registrarse con una aplicación para poder consumirlos. Esto debe ocurrir en el método CreateMauiApp de la clase MauiProgram del proyecto de aplicación, que es el punto de entrada multiplataforma de la aplicación:

using VideoDemos.Controls;
using VideoDemos.Handlers;

namespace VideoDemos;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler(typeof(Video), typeof(VideoHandler));
            });

        return builder.Build();
    }
}

El controlador se registra con los métodos ConfigureMauiHandlers y AddHandler. El primer argumento para el método AddHandler es el tipo de control multiplataforma, siendo el segundo argumento su tipo de controlador.

Consumo del control multiplataforma

Después de registrar el controlador con la aplicación, se puede consumir el control multiplataforma.

Reproducción de un vídeo web

El control Video puede reproducir un vídeo desde una dirección URL, como se muestra en el ejemplo siguiente:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.PlayWebVideoPage"
             Unloaded="OnContentPageUnloaded"
             Title="Play web video">
    <controls:Video x:Name="video"
                    Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>

En este ejemplo, la clase VideoSourceConverter convierte la cadena que representa el URI en un UriVideoSource. Se empieza a cargar el vídeo y se inicia la reproducción cuando se ha descargado y almacenado en búfer una cantidad de datos suficiente. En cada una de las plataformas, los controles de transporte se atenúan si no se usan, pero puedes pulsar en el vídeo para restaurarlos y verlos.

Reproducir recurso de vídeo

Los archivos de vídeo incrustados en la carpeta Resources\Raw de la aplicación, con una acción de compilación MauiAsset, se pueden reproducir mediante el control Video:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.PlayVideoResourcePage"
             Unloaded="OnContentPageUnloaded"
             Title="Play video resource">
    <controls:Video x:Name="video"
                    Source="video.mp4" />
</ContentPage>

En este ejemplo, la clase VideoSourceConverter convierte la cadena que representa el nombre de archivo del vídeo en un ResourceVideoSource. En todas las plataformas, el vídeo empieza a reproducirse casi inmediatamente después de establecer el origen de vídeo, porque el archivo se encuentra en el paquete de la aplicación y no debe descargarse. En cada una de las plataformas, los controles de transporte se atenúan si no se usan, pero puedes pulsar en el vídeo para restaurarlos y verlos.

Reproducir un archivo de vídeo desde la biblioteca del dispositivo

Los archivos de vídeo almacenados en el dispositivo se pueden recuperar y reproducir mediante el control Video:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.PlayLibraryVideoPage"
             Unloaded="OnContentPageUnloaded"
             Title="Play library video">
    <Grid RowDefinitions="*,Auto">
        <controls:Video x:Name="video" />
        <Button Grid.Row="1"
                Text="Show Video Library"
                Margin="10"
                HorizontalOptions="Center"
                Clicked="OnShowVideoLibraryClicked" />
    </Grid>
</ContentPage>

Cuando se pulsa Button, se ejecuta su controlador de eventos Clicked, como se muestra en el ejemplo de código siguiente:

async void OnShowVideoLibraryClicked(object sender, EventArgs e)
{
    Button button = sender as Button;
    button.IsEnabled = false;

    var pickedVideo = await MediaPicker.PickVideoAsync();
    if (!string.IsNullOrWhiteSpace(pickedVideo?.FileName))
    {
        video.Source = new FileVideoSource
        {
            File = pickedVideo.FullPath
        };
    }

    button.IsEnabled = true;
}

El controlador de eventos Clicked usa la clase MediaPicker de .NET MAUI para permitir al usuario elegir un archivo de vídeo del dispositivo. El archivo de vídeo seleccionado se encapsula como un objeto FileVideoSource y se establece como la propiedad Source del control Video. Para obtener más información sobre la clase MediaPicker, consulta Selector de archivos multimedia. En todas las plataformas, el vídeo empieza a reproducirse casi inmediatamente después de establecer el origen de vídeo porque el archivo se encuentra en el dispositivo y no debe descargarse. En cada una de las plataformas, los controles de transporte se atenúan si no se usan, pero puedes pulsar en el vídeo para restaurarlos y verlos.

Configuración del control de vídeo

Para impedir que un vídeo se inicie de forma automática, establece la propiedad AutoPlay en false:

<controls:Video x:Name="video"
                Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
                AutoPlay="False" />

Puedes suprimir los controles de transporte si estableces la propiedad AreTransportControlsEnabled en false:

<controls:Video x:Name="video"
                Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
                AreTransportControlsEnabled="False" />

Si estableces AutoPlay y AreTransportControlsEnabled en false, el vídeo no empezará a reproducirse y no habrá ninguna manera de iniciarlo. En este escenario, tendrías que llamar al método Play desde el archivo de código subyacente o crear tus propios controles de transporte.

Además, puedes establecer un bucle de vídeo estableciendo la propiedad IsLooping en true:

<controls:Video x:Name="video"
                Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
                IsLooping="true" />

Si estableces la propiedad IsLooping en true, esto garantiza que el control Video establezca automáticamente la posición del vídeo al principio después de alcanzar su fin.

Uso de controles de transporte personalizados

En el ejemplo XAML siguiente se muestran controles de transporte personalizados que reproducen, pausan y detienen el vídeo:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.CustomTransportPage"
             Unloaded="OnContentPageUnloaded"
             Title="Custom transport controls">
    <Grid RowDefinitions="*,Auto">
        <controls:Video x:Name="video"
                        AutoPlay="False"
                        AreTransportControlsEnabled="False"
                        Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
        <ActivityIndicator Color="Gray"
                           IsVisible="False">
            <ActivityIndicator.Triggers>
                <DataTrigger TargetType="ActivityIndicator"
                             Binding="{Binding Source={x:Reference video},
                                               Path=Status}"
                             Value="{x:Static controls:VideoStatus.NotReady}">
                    <Setter Property="IsVisible"
                            Value="True" />
                    <Setter Property="IsRunning"
                            Value="True" />
                </DataTrigger>
            </ActivityIndicator.Triggers>
        </ActivityIndicator>
        <Grid Grid.Row="1"
              Margin="0,10"
              ColumnDefinitions="0.5*,0.5*"
              BindingContext="{x:Reference video}">
            <Button Text="&#x25B6;&#xFE0F; Play"
                    HorizontalOptions="Center"
                    Clicked="OnPlayPauseButtonClicked">
                <Button.Triggers>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.Playing}">
                        <Setter Property="Text"
                                Value="&#x23F8; Pause" />
                    </DataTrigger>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.NotReady}">
                        <Setter Property="IsEnabled"
                                Value="False" />
                    </DataTrigger>
                </Button.Triggers>
            </Button>
            <Button Grid.Column="1"
                    Text="&#x23F9; Stop"
                    HorizontalOptions="Center"
                    Clicked="OnStopButtonClicked">
                <Button.Triggers>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.NotReady}">
                        <Setter Property="IsEnabled"
                                Value="False" />
                    </DataTrigger>
                </Button.Triggers>
            </Button>
        </Grid>
    </Grid>
</ContentPage>

En este ejemplo, el control Video establece la propiedad AreTransportControlsEnabled en false y define un Button que reproduce y pausa el vídeo, y un Button que detiene la reproducción del vídeo. La apariencia del botón se define mediante caracteres unicode y sus equivalentes de texto, para crear botones que constan de un icono y texto:

Screenshot of play and pause buttons.

Cuando se reproduce el vídeo, el botón de reproducción cambia a un botón de pausa:

Screenshot of pause and stop buttons.

La interfaz de usuario también incluye un ActivityIndicator que se muestra mientras se carga el vídeo. Los desencadenadores de datos se usan para habilitar y deshabilitar el ActivityIndicator y los botones, así como para cambiar el primer botón entre reproducción y pausa. Para obtener más información sobre los desencadenadores de datos, consulta Desencadenadores de datos.

El archivo de código subyacente define los controladores de los eventos Clicked del botón:

public partial class CustomTransportPage : ContentPage
{
    ...
    void OnPlayPauseButtonClicked(object sender, EventArgs args)
    {
        if (video.Status == VideoStatus.Playing)
        {
            video.Pause();
        }
        else if (video.Status == VideoStatus.Paused)
        {
            video.Play();
        }
    }

    void OnStopButtonClicked(object sender, EventArgs args)
    {
        video.Stop();
    }
    ...
}

Barra de posicionamiento personalizada

En el ejemplo siguiente se muestra una barra de posicionamiento personalizada, PositionSlider, que se usa en XAML:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.CustomPositionBarPage"
             Unloaded="OnContentPageUnloaded"
             Title="Custom position bar">
    <Grid RowDefinitions="*,Auto,Auto">
        <controls:Video x:Name="video"
                        AreTransportControlsEnabled="False"
                        Source="{StaticResource ElephantsDream}" />
        ...
        <Grid Grid.Row="1"
              Margin="10,0"
              ColumnDefinitions="0.25*,0.25*,0.25*,0.25*"
              BindingContext="{x:Reference video}">
            <Label Text="{Binding Path=Position,
                                  StringFormat='{0:hh\\:mm\\:ss}'}"
                   HorizontalOptions="Center"
                   VerticalOptions="Center" />
            ...
            <Label Grid.Column="3"
                   Text="{Binding Path=TimeToEnd,
                                  StringFormat='{0:hh\\:mm\\:ss}'}"
                   HorizontalOptions="Center"
                   VerticalOptions="Center" />
        </Grid>
        <controls:PositionSlider Grid.Row="2"
                                 Margin="10,0,10,10"
                                 BindingContext="{x:Reference video}"
                                 Duration="{Binding Duration}"
                                 Position="{Binding Position}">
            <controls:PositionSlider.Triggers>
                <DataTrigger TargetType="controls:PositionSlider"
                             Binding="{Binding Status}"
                             Value="{x:Static controls:VideoStatus.NotReady}">
                    <Setter Property="IsEnabled"
                            Value="False" />
                </DataTrigger>
            </controls:PositionSlider.Triggers>
        </controls:PositionSlider>
    </Grid>
</ContentPage>

La propiedad Position del objeto Video está enlazada a la propiedad Position de PositionSlider, sin problemas de rendimiento, porque el método MauiVideoPlayer.UpdateStatus cambia la propiedad Video.Position en cada plataforma, lo que solo se llama 10 veces por segundo. Además, dos objetos Label muestran los valores de las propiedades Position y TimeToEnd del objeto Video.

Limpieza de vista nativa

La implementación del controlador de cada plataforma invalida la implementación DisconnectHandler, que se usa para realizar la limpieza de vista nativa, como la cancelación de la suscripción a eventos y la eliminación de objetos. Sin embargo, .NET MAUI no invoca intencionadamente esta invalidación. En su lugar, debes invocarlo tu mismo desde una ubicación adecuada en el ciclo de vida de la aplicación. Esto suele ocurrir cuando la página que contiene el control Video se aleja, lo que hace que se genere el evento Unloaded de la página.

Un controlador de eventos para el evento Unloaded de la página se puede registrar en XAML:

<ContentPage ...
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             Unloaded="OnContentPageUnloaded">
    <controls:Video x:Name="video"
                    ... />
</ContentPage>

El controlador de eventos para el evento Unloaded puede invocar el método DisconnectHandler en su instancia de Handler:

void OnContentPageUnloaded(object sender, EventArgs e)
{
    video.Handler?.DisconnectHandler();
}

Además de limpiar los recursos de vista nativos, invocar el método DisconnectHandler del controlador también garantiza que los vídeos dejen de reproducirse en la navegación hacia atrás en iOS.