Creación de un control personalizado mediante controladores
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:
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:
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:
- 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.
- Crea cualquier tipo de multiplataforma adicional necesario.
- Crea una clase de controlador
partial
. Para obtener más información, consultaCreación del controlador. - En la clase de controlador, crea un diccionario PropertyMapper, que define las acciones que se realizarán cuando se produzcan cambios en las propiedades multiplataforma. Para obtener más información, consulta Creación del asignador de propiedades.
- 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.
- 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. - Registra el controla dor mediante los métodos ConfigureMauiHandlers y AddHandler en la clase
MauiProgram
de la aplicación. ara obtener 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á a 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.
Crea el 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 uso en todas las plataformas. La alternativa sería tener que definir la propiedad PlatformView
una vez por plataforma mediante la 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 de MAUI ViewHandler<TVirtualView,TPlatformView> de .NET y requiere que se proporcionen dos argumentos genéricos:
- La clase para el control multiplataforma, que deriva de View.
- La clase para el controlador.
En el ejemplo de código siguiente se muestra la clase VideoHandler
ampliada con la definición de 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 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 actualizará 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 de MAUI ViewHandler<TVirtualView,TPlatformView> de .NET y requiere que se proporcionen dos argumentos genéricos:
- La clase para el control multiplataforma, que deriva de View.
- La clase para el controlador.
En el ejemplo de código siguiente se muestra la clase VideoHandler
ampliada con la definición de 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 clases parciales en las carpetas secundarias de la carpeta Plataformas. 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:
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 ViewHandler<TVirtualView,TPlatformView> clase , que requiere dos argumentos de tipo:
- La clase para el control multiplataforma, que deriva de View.
- El tipo de la vista nativa que implementa el control multiplataforma en la plataforma. Este debe ser idéntico al tipo de la propiedad
PlatformView
en el controlador.
Importante
La clase ViewHandler<TVirtualView,TPlatformView> proporciona 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 obtener más información, consulta Limpieza de la vista nativa.
Importante
.NET MAUI invoca automáticamente el DisconnectHandler método de forma predeterminada, aunque este comportamiento se puede cambiar. Para obtener más información, consulte Desconexión del controlador de control.
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<TVirtualView,TPlatformView>, 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 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 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<TVirtualView,TPlatformView>, 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 facilita 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<TVirtualView,TPlatformView>, 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 Path
propiedad 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
establezca la propiedad Status
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 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 consumirlo. 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 el método 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="▶️ Play"
HorizontalOptions="Center"
Clicked="OnPlayPauseButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.Playing}">
<Setter Property="Text"
Value="⏸ 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="⏹ 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:
Cuando se reproduce el vídeo, el botón de reproducción cambia a un botón de pausa:
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 vistas nativas, como la anulación de la suscripción a eventos y la eliminación de objetos. Pero .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.
Desconexión del controlador de control
La implementación del controlador de cada plataforma invalida la implementación DisconnectHandler, que se usa para realizar la limpieza de vistas nativas, como la anulación de la suscripción a eventos y la eliminación de objetos. De forma predeterminada, los controladores se desconectan automáticamente de sus controles siempre que sea posible, como al navegar hacia atrás en una aplicación.
En algunos escenarios, es posible que quiera controlar cuándo un controlador se desconecta de su control, lo que se puede lograr con la HandlerProperties.DisconnectPolicy
propiedad adjunta. Esta propiedad requiere un HandlerDisconnectPolicy argumento, con la enumeración que define los siguientes valores:
Automatic
, que indica que el controlador se desconectará automáticamente. Se trata del valor predeterminado de la propiedad adjuntaHandlerProperties.DisconnectPolicy
.Manual
, que indica que el controlador tendrá que desconectarse manualmente invocando la DisconnectHandler() implementación.
En el ejemplo siguiente se muestra cómo establecer la propiedad adjunta de HandlerProperties.DisconnectPolicy
:
<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />
El código de C# equivalente es el siguiente:
Video video = new Video
{
Source = "video.mp4",
AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video, HandlerDisconnectPolicy.Manual);
Al establecer la HandlerProperties.DisconnectPolicy
propiedad Manual
adjunta en , debe invocar la implementación del DisconnectHandler controlador usted mismo desde una ubicación adecuada en el ciclo de vida de la aplicación. Esto se puede lograr invocando video.Handler?.DisconnectHandler();
.
Además, hay un método de extensión de DisconnectHandlers que desconecta los controladores de un determinado IView:
video.DisconnectHandlers();
Al desconectarlo, el método de DisconnectHandlers propagará el árbol de control hasta que se complete o llegue a un control que haya establecido una directiva manual.