Criar um controle personalizado usando manipuladores

Procurar amostra. Procurar no exemplo

Um requisito padrão para aplicativos é a capacidade de reproduzir vídeos. Este artigo examina como criar um controle multiplataforma Video .NET MAUI (Interface do Usuário do Aplicativo Multiplataforma) do .NET que usa um manipulador para mapear a API de controle multiplataforma para as exibições nativas no Android, iOS e Mac Catalyst que reproduzem vídeos. Esse controle pode reproduzir vídeo de três fontes:

  • Uma URL, que representa um vídeo remoto.
  • Um recurso, que é um arquivo inserido no aplicativo.
  • Um arquivo, da biblioteca de vídeos do dispositivo.

Os controles de vídeo exigem controles de transporte, que são botões para reproduzir e pausar o vídeo, e uma barra de posicionamento que mostra o progresso do vídeo e permite que o usuário se mova rapidamente para um local diferente. O Video controle pode usar os controles de transporte e a barra de posicionamento fornecidos pela plataforma ou você pode fornecer controles de transporte personalizados e uma barra de posicionamento. As capturas de tela a seguir mostram o controle no iOS, com e sem controles de transporte personalizados:

Captura de tela da reprodução de vídeo no iOS.Captura de tela da reprodução de vídeo usando controles de transporte personalizados no iOS.

Um controle de vídeo mais sofisticado teria recursos adicionais, como um controle de volume, um mecanismo para interromper a reprodução de vídeo quando uma chamada é recebida e uma maneira de manter a tela ativa durante a reprodução.

A arquitetura do Video controle é mostrada no diagrama a seguir:

Arquitetura do manipulador de vídeo.

A Video classe fornece a API multiplataforma para o controle. O mapeamento da API multiplataforma para as APIs de visualização nativa é executado pela VideoHandler classe em cada plataforma, que mapeia a Video classe para a MauiVideoPlayer classe. No iOS e no Mac Catalyst, a MauiVideoPlayer classe usa o AVPlayer tipo para fornecer reprodução de vídeo. No Android, a MauiVideoPlayer classe usa o VideoView tipo para fornecer reprodução de vídeo. No Windows, a MauiVideoPlayer classe usa o MediaPlayerElement tipo para fornecer reprodução de vídeo.

Importante

O .NET MAUI separa seus manipuladores de seus controles multiplataforma por meio de interfaces. Isso permite que estruturas experimentais, como Comet e Fabulous, forneçam seus próprios controles multiplataforma, que implementam as interfaces, enquanto ainda usam os manipuladores do .NET MAUI. A criação de uma interface para o controle multiplataforma só será necessária se você precisar desacoplar o manipulador do controle multiplataforma para uma finalidade semelhante ou para fins de teste.

O processo para criar um controle personalizado .NET MAUI multiplataforma, cujas implementações de plataforma são fornecidas por manipuladores, é o seguinte:

  1. Crie uma classe para o controle multiplataforma, que fornece a API pública do controle. Para obter mais informações, consulte Criar o controle multiplataforma.
  2. Crie todos os tipos de plataforma cruzada adicionais necessários.
  3. Crie uma partial classe de manipulador. Para obter mais informações, consulte Criar o manipulador.
  4. Na classe do manipulador, crie um PropertyMapper dicionário, que define as ações a serem executadas quando ocorrerem alterações de propriedade entre plataformas. Para obter mais informações, consulte Criar o mapeador de propriedades.
  5. Opcionalmente, em sua classe de manipulador, crie um CommandMapper dicionário, que define as ações a serem executadas quando o controle multiplataforma envia instruções para as exibições nativas que implementam o controle multiplataforma. Para obter mais informações, consulte Criar o mapeador de comandos.
  6. Crie partial classes de manipulador para cada plataforma que criam as exibições nativas que implementam o controle multiplataforma. Para obter mais informações, consulte Criar os controles da plataforma.
  7. Registre o manipulador usando os ConfigureMauiHandlers métodos and AddHandler na classe do MauiProgram aplicativo. Para obter mais informações, consulte Registrar o manipulador.

Em seguida, o controle multiplataforma pode ser consumido. Para obter mais informações, consulte Consumir o controle multiplataforma.

Criar o controle multiplataforma

Para criar um controle multiplataforma, você deve criar uma classe 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); }
        }        
        ...
    }
}

O controle deve fornecer uma API pública que será acessada por seu manipulador e controlar os consumidores. Os controles multiplataforma devem derivar de View, que representa um elemento visual usado para colocar layouts e exibições na tela.

Criar o manipulador

Depois de criar seu controle multiplataforma, você deve criar uma partial classe para seu manipulador:

#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
    {
    }
}

A classe handler é uma classe parcial cuja implementação será concluída em cada plataforma com uma classe parcial adicional.

As instruções condicionais using definem o PlatformView tipo em cada plataforma. No Android, iOS, Mac Catalyst e Windows, as exibições nativas são fornecidas pela classe personalizada MauiVideoPlayer . A instrução condicional using final é definida PlatformView como igual a System.Object. Isso é necessário para que o PlatformView tipo possa ser usado dentro do manipulador para uso em todas as plataformas. A alternativa seria ter que definir a propriedade uma vez por plataforma, usando compilação PlatformView condicional.

Criar o mapeador de propriedades

Cada manipulador normalmente fornece um mapeador de propriedades, que define quais ações devem ser executadas quando ocorre uma alteração de propriedade no controle multiplataforma. O PropertyMapper tipo é um Dictionary que mapeia as propriedades do controle multiplataforma para suas ações associadas.

PropertyMapper é definido na classe do .NET MAUI e requer que dois argumentos genéricos ViewHandler<TVirtualView,TPlatformView> sejam fornecidos:

  • A classe para o controle multiplataforma, que deriva de View.
  • A classe para o manipulador.

O exemplo de código a seguir mostra a VideoHandler classe estendida com a PropertyMapper definição:

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)
    {
    }
}

O PropertyMapper é um Dictionary cuja chave é um string e cujo valor é um genérico Action. O string representa o nome da propriedade do controle multiplataforma e o Action representa um static método que requer o manipulador e o controle multiplataforma como argumentos. Por exemplo, a assinatura do MapSource método é public static void MapSource(VideoHandler handler, Video video).

Cada manipulador de plataforma deve fornecer implementações das Ações, que manipulam as APIs de exibição nativas. Isso garante que, quando uma propriedade for definida em um controle multiplataforma, a exibição nativa subjacente será atualizada conforme necessário. A vantagem dessa abordagem é que ela permite fácil personalização de controle entre plataformas, pois o mapeador de propriedades pode ser modificado por consumidores de controle entre plataformas sem subclasses.

Criar o mapeador de comandos

Cada manipulador também pode fornecer um mapeador de comandos, que define quais ações executar quando o controle multiplataforma envia comandos para exibições nativas. Os mapeadores de comando são semelhantes aos mapeadores de propriedades, mas permitem que dados adicionais sejam passados. Nesse contexto, um comando é uma instrução e, opcionalmente, seus dados, que são enviados para uma exibição nativa. O CommandMapper tipo é um Dictionary que mapeia membros de controle multiplataforma para suas ações associadas.

CommandMapper é definido na classe do .NET MAUI e requer que dois argumentos genéricos ViewHandler<TVirtualView,TPlatformView> sejam fornecidos:

  • A classe para o controle multiplataforma, que deriva de View.
  • A classe para o manipulador.

O exemplo de código a seguir mostra a VideoHandler classe estendida com a CommandMapper definição:

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)
    {
    }
}

O CommandMapper é um Dictionary cuja chave é um string e cujo valor é um genérico Action. O string representa o nome do comando do controle multiplataforma e o Action representa um static método que requer o manipulador, o controle multiplataforma e os dados opcionais como argumentos. Por exemplo, a assinatura do MapPlayRequested método é public static void MapPlayRequested(VideoHandler handler, Video video, object? args).

Cada manipulador de plataforma deve fornecer implementações das Ações, que manipulam as APIs de exibição nativas. Isso garante que, quando um comando for enviado do controle multiplataforma, a exibição nativa subjacente será manipulada conforme necessário. A vantagem dessa abordagem é que ela elimina a necessidade de exibições nativas para assinar e cancelar a assinatura de eventos de controle entre plataformas. Além disso, permite fácil personalização porque o mapeador de comandos pode ser modificado por consumidores de controle multiplataforma sem subclasse.

Criar os controles da plataforma

Depois de criar os mapeadores para o manipulador, você deve fornecer implementações de manipulador em todas as plataformas. Isso pode ser feito adicionando implementações parciais de manipulador de classe nas pastas filho da pasta Plataformas . Como alternativa, você pode configurar seu projeto para dar suporte a vários destinos baseados em nome de arquivo ou vários destinos baseados em pasta, ou ambos.

O aplicativo de exemplo é configurado para dar suporte a vários destinos baseados em nome de arquivo, de modo que todas as classes de manipulador estejam localizadas em uma única pasta:

Captura de tela dos arquivos na pasta Handlers do projeto.

A VideoHandler classe que contém os mapeadores é chamada VideoHandler.cs. Suas implementações de plataforma estão nos arquivos VideoHandler.Android.cs, VideoHandler.MaciOS.cs e VideoHandler.Windows.cs . Essa multiplataforma baseada em nome de arquivo é configurada adicionando o seguinte XML ao arquivo de projeto, como filhos do <Project> nó:

<!-- 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 obter mais informações sobre como configurar vários destinos, consulte Configurar vários destinos.

Cada classe de manipulador de plataforma deve ser uma classe parcial e derivar da classe, o ViewHandler<TVirtualView,TPlatformView> que requer dois argumentos de tipo:

  • A classe para o controle multiplataforma, que deriva de View.
  • O tipo de exibição nativa que implementa o controle multiplataforma na plataforma. Isso deve ser idêntico ao tipo da PlatformView propriedade no manipulador.

Importante

A ViewHandler<TVirtualView,TPlatformView> classe fornece VirtualView e PlatformView propriedades. A VirtualView propriedade é usada para acessar o controle multiplataforma de seu manipulador. A PlatformView propriedade é usada para acessar a exibição nativa em cada plataforma que implementa o controle multiplataforma.

Cada uma das implementações do manipulador de plataforma deve substituir os seguintes métodos:

  • CreatePlatformView, que deve criar e retornar a exibição nativa que implementa o controle multiplataforma.
  • ConnectHandler, que deve executar qualquer configuração de exibição nativa, como inicializar a exibição nativa e executar assinaturas de eventos.
  • DisconnectHandler, que deve executar qualquer limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos.

Importante

O DisconnectHandler método não é invocado intencionalmente pelo .NET MAUI. Em vez disso, você deve invocá-lo por conta própria de um local adequado no ciclo de vida do seu aplicativo. Para obter mais informações, consulte Limpeza de exibição nativa.

Importante

O DisconnectHandler método é invocado automaticamente pelo .NET MAUI por padrão, embora esse comportamento possa ser alterado. Para obter mais informações, consulte Desconexão do manipulador de controle.

Cada manipulador de plataforma também deve implementar as ações definidas nos dicionários do mapeador.

Além disso, cada manipulador de plataforma também deve fornecer código, conforme necessário, para implementar a funcionalidade do controle multiplataforma na plataforma. Em alternativa, pode ser assegurado por um tipo adicional, que é a abordagem adotada no presente processo.

Android

O vídeo é reproduzido no Android com um VideoViewdomínio . No entanto, aqui, o VideoView foi encapsulado em um MauiVideoPlayer tipo para manter a exibição nativa separada de seu manipulador. O exemplo a seguir mostra a VideoHandler classe partial para Android, com suas três substituições:

#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 da ViewHandler<TVirtualView,TPlatformView> classe, com o argumento genérico Video especificando o tipo de controle multiplataforma e o MauiVideoPlayer argumento especificando o tipo que encapsula a VideoView exibição nativa.

A CreatePlatformView substituição cria e retorna um MauiVideoPlayer objeto. A ConnectHandler substituição é o local para executar qualquer configuração de exibição nativa necessária. A DisconnectHandler substituição é o local para executar qualquer limpeza de exibição nativa e, portanto, chama o Dispose método na MauiVideoPlayer instância.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador de propriedades:

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 ação é executada em resposta a uma alteração de propriedade no controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada como argumentos. Em cada caso, a ação chama um método definido no MauiVideoPlayer tipo.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador 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 ação é executada em resposta a um comando enviado do controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, após extrair os dados opcionais.

No Android, a MauiVideoPlayer classe encapsula o VideoView para manter a visualização nativa separada de seu manipulador:

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 a exibição nativa raiz em um aplicativo .NET MAUI no Android é CoordinatorLayout. Embora a MauiVideoPlayer classe possa derivar de outros tipos nativos do Android, pode ser difícil controlar o posicionamento da visualização nativa em alguns cenários.

Eles VideoView podem ser adicionados diretamente ao CoordinatorLayout, e posicionados no layout conforme necessário. No entanto, aqui, um Android RelativeLayout é adicionado ao CoordinatorLayout, e o VideoView é adicionado ao RelativeLayout. Os parâmetros de layout são definidos em e RelativeLayout VideoView para que o VideoView seja centralizado na página e se expanda para preencher o espaço disponível, mantendo sua taxa de proporção.

O construtor também assina o VideoView.Prepared evento. Esse evento é gerado quando o vídeo está pronto para reprodução e a assinatura é cancelada na Dispose substituição:

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);
    }
    ...
}

Além de cancelar a assinatura do evento, a substituição também executa a Prepared Dispose limpeza de exibição nativa.

Observação

A Dispose substituição é chamada pela substituição do DisconnectHandler manipulador.

Os controles de transporte da plataforma incluem botões que reproduzem, pausam e param o vídeo e são fornecidos pelo tipo do MediaController Android. Se a Video.AreTransportControlsEnabled propriedade for definida como true, a MediaController será definido como o reprodutor de mídia do VideoView. Isso ocorre porque, quando a AreTransportControlsEnabled propriedade é definida, o mapeador de propriedades do manipulador garante que o MapAreTransportControlsEnabled método seja invocado, o que, por sua vez, chama o UpdateTransportControlsEnabled método em 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;
            }
        }
    }
    ...
}

Os controles de transporte desaparecem se não forem usados, mas podem ser restaurados tocando no vídeo.

Se a Video.AreTransportControlsEnabled propriedade for definida como false, o será MediaController removido como o reprodutor de mídia do VideoView. Nesse cenário, você pode controlar a reprodução de vídeo programaticamente ou fornecer seus próprios controles de transporte. Para obter mais informações, consulte Criar controles de transporte personalizados.

Catalisador para iOS e Mac

O vídeo é reproduzido no iOS e no Mac Catalyst com um AVPlayer e um AVPlayerViewController. No entanto, aqui, esses tipos são encapsulados em um MauiVideoPlayer tipo para manter as exibições nativas separadas de seu manipulador. O exemplo a seguir mostra a VideoHandler classe parcial para iOS, com suas três substituições:

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 da ViewHandler<TVirtualView,TPlatformView> classe, com o argumento genérico Video especificando o tipo de controle multiplataforma e o MauiVideoPlayer argumento especificando o tipo que encapsula as AVPlayer exibições e AVPlayerViewController nativas.

A CreatePlatformView substituição cria e retorna um MauiVideoPlayer objeto. A ConnectHandler substituição é o local para executar qualquer configuração de exibição nativa necessária. A DisconnectHandler substituição é o local para executar qualquer limpeza de exibição nativa e, portanto, chama o Dispose método na MauiVideoPlayer instância.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador de propriedades:

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 ação é executada em resposta a uma alteração de propriedade no controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada como argumentos. Em cada caso, a ação chama um método definido no MauiVideoPlayer tipo.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador 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 ação é executada em resposta a um comando enviado do controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, após extrair os dados opcionais.

No iOS e no Mac Catalyst, a MauiVideoPlayer classe encapsula os AVPlayer tipos and AVPlayerViewController para manter as exibições nativas separadas de seu manipulador:

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 é a classe base no iOS e no Mac Catalyst para objetos que exibem conteúdo e lidam com a interação do usuário com esse conteúdo. O construtor cria um AVPlayer objeto, que gerencia a reprodução e o tempo de um arquivo de mídia e o define como o Player valor de propriedade de um AVPlayerViewControllerarquivo . O AVPlayerViewController exibe o conteúdo do AVPlayer e apresenta controles de transporte e outros recursos. O tamanho e o local do controle são definidos, o que garante que o vídeo seja centralizado na página e expandido para preencher o espaço disponível, mantendo sua taxa de proporção. No iOS 16 e no Mac Catalyst 16, o AVPlayerViewController deve ser adicionado ao pai ViewController para aplicativos baseados em Shell, caso contrário, os controles de transporte não serão exibidos. A exibição nativa, que é a exibição do AVPlayerViewController, é adicionada à página.

O Dispose método é responsável por executar a limpeza de exibição 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);
    }
    ...
}

Em alguns cenários, os vídeos continuam sendo reproduzidos depois que uma página de reprodução de vídeo é navegada para fora. Para interromper o vídeo, o ReplaceCurrentItemWithPlayerItem é definido como null na Dispose substituição e outra limpeza de exibição nativa é executada.

Observação

A Dispose substituição é chamada pela substituição do DisconnectHandler manipulador.

Os controles de transporte da plataforma incluem botões que reproduzem, pausam e param o vídeo e são fornecidos pelo AVPlayerViewController tipo. Se a Video.AreTransportControlsEnabled propriedade estiver definida como true, o AVPlayerViewController exibirá seus controles de reprodução. Isso ocorre porque, quando a AreTransportControlsEnabled propriedade é definida, o mapeador de propriedades do manipulador garante que o MapAreTransportControlsEnabled método seja invocado, o que, por sua vez, chama o UpdateTransportControlsEnabled método em MauiVideoPlayer:

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

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

Os controles de transporte desaparecem se não forem usados, mas podem ser restaurados tocando no vídeo.

Se a Video.AreTransportControlsEnabled propriedade estiver definida como false, o AVPlayerViewController não mostrará seus controles de reprodução. Nesse cenário, você pode controlar a reprodução de vídeo programaticamente ou fornecer seus próprios controles de transporte. Para obter mais informações, consulte Criar controles de transporte personalizados.

Windows

O vídeo é reproduzido no Windows com o MediaPlayerElement. No entanto, aqui, o MediaPlayerElement foi encapsulado em um MauiVideoPlayer tipo para manter a exibição nativa separada de seu manipulador. O exemplo a seguir mostra a VideoHandler classe parcial para Windows, com suas três substituições:

#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 da ViewHandler<TVirtualView,TPlatformView> classe, com o argumento genérico Video especificando o tipo de controle multiplataforma e o MauiVideoPlayer argumento especificando o tipo que encapsula a MediaPlayerElement exibição nativa.

A CreatePlatformView substituição cria e retorna um MauiVideoPlayer objeto. A ConnectHandler substituição é o local para executar qualquer configuração de exibição nativa necessária. A DisconnectHandler substituição é o local para executar qualquer limpeza de exibição nativa e, portanto, chama o Dispose método na MauiVideoPlayer instância.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador de propriedades:

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 ação é executada em resposta a uma alteração de propriedade no controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada como argumentos. Em cada caso, a ação chama um método definido no MauiVideoPlayer tipo.

O manipulador de plataforma também precisa implementar as ações definidas no dicionário do mapeador 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 ação é executada em resposta a um comando enviado do controle multiplataforma e é um static método que requer instâncias de controle de manipulador e plataforma cruzada e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, após extrair os dados opcionais.

No Windows, a MauiVideoPlayer classe encapsula o MediaPlayerElement para manter a exibição nativa separada de seu manipulador:

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, e o MediaPlayerElement é adicionado como um filho do Grid. Isso permite que o dimensione automaticamente para preencher todo o MediaPlayerElement espaço disponível.

O Dispose método é responsável por executar a limpeza de exibição 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;
    }
    ...
}

Além de cancelar a assinatura do evento, a substituição também executa a MediaOpened Dispose limpeza de exibição nativa.

Observação

A Dispose substituição é chamada pela substituição do DisconnectHandler manipulador.

Os controles de transporte da plataforma incluem botões que reproduzem, pausam e param o vídeo e são fornecidos pelo MediaPlayerElement tipo. Se a Video.AreTransportControlsEnabled propriedade estiver definida como true, o MediaPlayerElement exibirá seus controles de reprodução. Isso ocorre porque, quando a AreTransportControlsEnabled propriedade é definida, o mapeador de propriedades do manipulador garante que o MapAreTransportControlsEnabled método seja invocado, o que, por sua vez, chama o UpdateTransportControlsEnabled método em MauiVideoPlayer:

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

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

}

Se a Video.AreTransportControlsEnabled propriedade estiver definida como false, o MediaPlayerElement não mostrará seus controles de reprodução. Nesse cenário, você pode controlar a reprodução de vídeo programaticamente ou fornecer seus próprios controles de transporte. Para obter mais informações, consulte Criar controles de transporte personalizados.

Converter um controle multiplataforma em um controle de plataforma

Qualquer controle multiplataforma .NET MAUI, derivado de , pode ser convertido em seu controle de plataforma subjacente com o ToPlatform método de Elementextensão:

  • No Android, ToPlatform converte um controle .NET MAUI em um objeto Android View .
  • No iOS e no Mac Catalyst, ToPlatform converte um controle .NET MAUI em um UIView objeto.
  • No Windows, ToPlatform converte um controle .NET MAUI em um FrameworkElement objeto.

Observação

O método ToPlatform está no namespace Microsoft.Maui.Platform.

Em todas as plataformas, o ToPlatform método requer um MauiContext argumento.

O ToPlatform método pode converter um controle multiplataforma em seu controle de plataforma subjacente do código da plataforma, como em uma classe de manipulador parcial para uma 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);
            ...
        }
        ...
    }
}

Neste exemplo, na VideoHandler classe partial para Android, o método converte MapSource a Video instância em um MauiVideoPlayer objeto.

O ToPlatform método também pode converter um controle multiplataforma em seu controle de plataforma subjacente a partir de 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
        ...
    }
    ...
}

Neste exemplo, um controle multiplataforma Video nomeado video é convertido em sua exibição nativa subjacente em cada plataforma na OnHandlerChanged() substituição. Essa substituição é chamada quando a exibição nativa que implementa o controle multiplataforma está disponível e inicializada. O objeto retornado pelo método pode ser convertido em ToPlatform seu tipo nativo exato, que aqui é um MauiVideoPlayer.

Reproduzir o vídeo

A Video classe define uma Source propriedade, que é usada para especificar a origem do arquivo de vídeo, e uma AutoPlay propriedade. AutoPlay O padrão é true, o que significa que o vídeo deve começar a ser reproduzido automaticamente após Source a definição. Para obter a definição dessas propriedades, consulte Criar o controle multiplataforma.

A Source propriedade é do tipo VideoSource, que é uma classe abstrata que consiste em três métodos estáticos que instanciam as três classes que derivam 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 };
        }
    }
}

A classe VideoSource inclui um atributo TypeConverter que referencia 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.");
        }
    }
}

O conversor de tipo é invocado quando a Source propriedade é definida como uma cadeia de caracteres em XAML. O método ConvertFromInvariantString tenta converter a cadeia de caracteres em um objeto Uri. Se for bem-sucedido e o esquema não filefor , o método retornará um UriVideoSource. Caso contrário, ele retornará um ResourceVideoSource.

Reproduzir um vídeo da Web

A UriVideoSource classe é usada para especificar um vídeo remoto com um URI. Ele define uma Uri propriedade do 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); }
        }
    }
}

Quando a Source propriedade é definida como um UriVideoSource, o mapeador de propriedades do manipulador garante que o MapSource método seja invocado:

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

O MapSource método, por sua vez, chama o UpdateSource método na propriedade do PlatformView manipulador. A PlatformView propriedade, que é do tipo MauiVideoPlayer, representa a exibição nativa que fornece a implementação do player de vídeo em cada plataforma.

Android

O vídeo é reproduzido no Android com um VideoViewdomínio . O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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();
            }
        }
        ...
    }
}

Ao processar objetos do tipo UriVideoSource, o SetVideoUri método de VideoView é usado para especificar o vídeo a ser reproduzido, com um objeto Android Uri criado a partir do URI da cadeia de caracteres.

A AutoPlay propriedade não tem equivalente em VideoView, portanto, o Start método é chamado se um novo vídeo tiver sido definido.

Catalisador para iOS e Mac

Para reproduzir um vídeo no iOS e no Mac Catalyst, um objeto do tipo AVAsset é criado para encapsular o vídeo e é usado para criar um AVPlayerItem, que é então entregue ao AVPlayer objeto. O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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();
            }
        }
        ...
    }
}

Ao processar objetos do tipo UriVideoSource, o método estático AVAsset.FromUrl é usado para especificar o vídeo a ser reproduzido, com um objeto iOS NSUrl criado a partir do URI da cadeia de caracteres.

A AutoPlay propriedade não tem equivalente nas classes de vídeo do iOS, portanto, a propriedade é examinada no final do UpdateSource método para chamar o Play método no AVPlayer objeto.

Em alguns casos no iOS, os vídeos continuam sendo reproduzidos depois que a página de reprodução do vídeo é removida. Para interromper o vídeo, o ReplaceCurrentItemWithPlayerItem é definido como null na Dispose substituição:

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

Windows

O vídeo é reproduzido no Windows com um MediaPlayerElementdomínio . O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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;
            }
        }
        ...
    }
}

Ao processar objetos do tipo UriVideoSource, a MediaPlayerElement.Source propriedade é definida como um MediaSource objeto que inicializa a Uri com o URI do vídeo a ser reproduzido. Quando o MediaPlayerElement.Source tiver sido definido, o OnMediaPlayerMediaOpened método do manipulador de eventos será registrado no MediaPlayerElement.MediaPlayer.MediaOpened evento. Esse manipulador de eventos é usado para definir a Duration propriedade do Video controle.

No final do método, a Video.AutoPlay propriedade é examinada e, se for verdadeira, a MediaPlayerElement.AutoPlay propriedade é definida como true para iniciar a reprodução do UpdateSource vídeo.

Reproduzir um recurso de vídeo

A ResourceVideoSource classe é usada para acessar arquivos de vídeo incorporados no aplicativo. Ele define uma Path propriedade do tipo string:

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); }
        }
    }
}

Quando a Source propriedade é definida como um ResourceVideoSource, o mapeador de propriedades do manipulador garante que o MapSource método seja invocado:

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

O MapSource método, por sua vez, chama o UpdateSource método na propriedade do PlatformView manipulador. A PlatformView propriedade, que é do tipo MauiVideoPlayer, representa a exibição nativa que fornece a implementação do player de vídeo em cada plataforma.

Android

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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;
                }
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo ResourceVideoSource, o SetVideoPath método de é usado para especificar o vídeo a ser reproduzido, com um argumento de cadeia de VideoView caracteres combinando o nome do pacote do aplicativo com o nome do arquivo do vídeo.

Um arquivo de vídeo de recurso é armazenado na pasta de ativos do pacote e requer um provedor de conteúdo para acessá-lo. O provedor de conteúdo é fornecido pela VideoProvider classe, que cria um AssetFileDescriptor objeto que fornece acesso ao arquivo 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;
        }
        ...
    }
}

Catalisador para iOS e Mac

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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);
                }
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo ResourceVideoSource, o GetUrlForResource método de NSBundle é usado para recuperar o arquivo do pacote do aplicativo. O caminho completo precisa ser dividido em um nome de arquivo, uma extensão e um diretório.

Em alguns casos no iOS, os vídeos continuam sendo reproduzidos depois que a página de reprodução do vídeo é removida. Para interromper o vídeo, o ReplaceCurrentItemWithPlayerItem é definido como null na Dispose substituição:

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

Windows

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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;
                }
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo ResourceVideoSource, a MediaPlayerElement.Source propriedade é definida como um MediaSource objeto que inicializa a com o Uri caminho do recurso de vídeo prefixado com ms-appx:///.

Reproduzir um arquivo de vídeo da biblioteca do dispositivo

A FileVideoSource classe é usada para acessar vídeos na biblioteca de vídeos do dispositivo. Ele define uma File propriedade do 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); }
        }
    }
}

Quando a Source propriedade é definida como um FileVideoSource, o mapeador de propriedades do manipulador garante que o MapSource método seja invocado:

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

O MapSource método, por sua vez, chama o UpdateSource método na propriedade do PlatformView manipulador. A PlatformView propriedade, que é do tipo MauiVideoPlayer, representa a exibição nativa que fornece a implementação do player de vídeo em cada plataforma.

Android

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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;
                }
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo FileVideoSource, o SetVideoPath método de é usado para especificar o arquivo de VideoView vídeo a ser reproduzido.

Catalisador para iOS e Mac

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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 }));
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo FileVideoSource, o método estático AVAsset.FromUrl é usado para especificar o arquivo de vídeo a ser reproduzido, com o NSUrl.CreateFileUrl método criando um objeto iOS NSUrl a partir do URI da cadeia de caracteres.

Windows

O exemplo de código a seguir mostra como o método processa UpdateSource a Source propriedade quando ela é do 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;
                }
            }
            ...
        }
        ...
    }
}

Ao processar objetos do tipo FileVideoSource, o nome do arquivo de vídeo é convertido em um StorageFile objeto. Em seguida, o MediaSource.CreateFromStorageFile método retorna um MediaSource objeto definido como o valor da MediaPlayerElement.Source propriedade.

Repetir um vídeo

A Video classe define uma IsLooping propriedade, que permite que o controle defina automaticamente a posição do vídeo para o início depois de atingir seu fim. O padrão é false, o que indica que os vídeos não são repetidos automaticamente.

Quando a IsLooping propriedade é definida, o mapeador de propriedades do manipulador garante que o MapIsLooping método seja invocado:

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

O MapIsLooping método, por sua vez, chama o UpdateIsLooping método na propriedade do PlatformView manipulador. A PlatformView propriedade, que é do tipo MauiVideoPlayer, representa a exibição nativa que fornece a implementação do player de vídeo em cada plataforma.

Android

O exemplo de código a seguir mostra como o método no Android permite o UpdateIsLooping loop 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 o loop de vídeo, a classe implementa MauiVideoPlayer a MediaPlayer.IOnPreparedListener interface. Essa interface define um OnPrepared retorno de chamada que é invocado quando a fonte de mídia está pronta para reprodução. Quando a Video.IsLooping propriedade é true, o UpdateIsLooping método é definido MauiVideoPlayer como o objeto que fornece o retorno de OnPrepared chamada. O retorno de chamada define a MediaPlayer.IsLooping propriedade como o valor da Video.IsLooping propriedade.

Catalisador para iOS e Mac

O exemplo de código a seguir mostra como o método no iOS e no Mac Catalyst permite o UpdateIsLooping loop 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);
        }
        ...
    }
}

No iOS e Mac Catalyst, uma notificação é usada para executar um retorno de chamada quando o vídeo é reproduzido até o fim. Quando a Video.IsLooping propriedade é true, o UpdateIsLooping método adiciona um observador para a AVPlayerItem.DidPlayToEndTimeNotification notificação e executa o PlayedToEnd método quando a notificação é recebida. Por sua vez, esse método retoma a reprodução desde o início do vídeo. Se a Video.IsLooping propriedade for false, o vídeo será pausado no final da reprodução.

Como MauiVideoPlayer o adiciona um observador para uma notificação, ele também deve remover o observador ao executar a limpeza de exibição nativa. Isso é feito na Dispose substituição:

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;
    }
    ...
}

A Dispose substituição chama o DestroyPlayedToEndObserver método que remove o observador da AVPlayerItem.DidPlayToEndTimeNotification notificação e que também invoca o Dispose método no NSObject.

Windows

O exemplo de código a seguir mostra como o método no Windows permite o UpdateIsLooping loop de vídeo:

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

Para habilitar o loop de vídeo, o UpdateIsLooping método define a MediaPlayerElement.MediaPlayer.IsLoopingEnabled propriedade como o valor da Video.IsLooping propriedade.

Criar controles de transporte personalizados

Os controles de transporte de um player de vídeo incluem botões que reproduzem, pausam e param o vídeo. Esses botões geralmente são identificados com ícones familiares em vez de texto, e os botões reproduzir e pausar geralmente são combinados em um botão.

Por padrão, o controle exibe os Video controles de transporte suportados por cada plataforma. No entanto, quando você define a AreTransportControlsEnabled propriedade como false, esses controles são suprimidos. Em seguida, você pode controlar a reprodução de vídeo programaticamente ou fornecer seus próprios controles de transporte.

A implementação de seus próprios controles de transporte exige que a Video classe seja capaz de notificar suas exibições nativas para reproduzir, pausar ou interromper o vídeo e saber o status atual da reprodução do vídeo. A Video classe define métodos chamados Play, Pause, e Stop que geram um evento correspondente e enviam um comando para o 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);
        }
    }
}

A VideoPositionEventArgs classe define uma Position propriedade que pode ser definida por meio de seu construtor. Essa propriedade representa a posição na qual a reprodução de vídeo foi iniciada, pausada ou interrompida.

A linha final nos Playmétodos , Pause, e Stop envia um comando e dados associados para VideoHandler. O CommandMapper for VideoHandler mapeia nomes de comando para ações que são executadas quando um comando é recebido. Por exemplo, quando VideoHandler recebe o PlayRequested comando, ele executa seu MapPlayRequested método. A vantagem dessa abordagem é que ela elimina a necessidade de exibições nativas para assinar e cancelar a assinatura de eventos de controle entre plataformas. Além disso, permite fácil personalização porque o mapeador de comandos pode ser modificado por consumidores de controle multiplataforma sem subclasse. Para obter mais informações sobre CommandMappero , consulte Criar o mapeador de comandos.

A MauiVideoPlayer implementação no Android, iOS e Mac Catalyst tem PlayRequested, PauseRequested, e StopRequested métodos que são executados em resposta ao Video controle de envio PlayRequested, PauseRequested, e comandos StopRequested . Cada método invoca um método em sua exibição nativa para reproduzir, pausar ou parar o vídeo. Por exemplo, o código a seguir mostra os PlayRequestedmétodos , PauseRequestede StopRequested no iOS e 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 um dos três métodos registra a posição em que o vídeo foi reproduzido, pausado ou interrompido, usando os dados enviados com o comando.

Esse mecanismo garante que, quando o Playmétodo , Pause, or Stop for invocado Video no controle, seu modo de exibição nativo seja instruído a reproduzir, pausar ou parar o vídeo e registrar a posição na qual o vídeo foi reproduzido, pausado ou interrompido. Tudo isso acontece usando uma abordagem desacoplada, sem que as visualizações nativas precisem se inscrever em eventos multiplataforma.

Status do vídeo

A implementação da funcionalidade de reprodução, pausa e parada não é suficiente para dar suporte a controles de transporte personalizados. Muitas vezes, a funcionalidade de reprodução e pausa deve ser implementada com o mesmo botão, que muda sua aparência para indicar se o vídeo está sendo reproduzido ou pausado no momento. Além disso, o botão nem deve ser ativado se o vídeo ainda não tiver sido carregado.

Esses requisitos implicam que o player de vídeo precisa disponibilizar um status atual que indica se ele está reproduzindo um vídeo ou em pausa ou se ele ainda não está pronto para reproduzir um vídeo. Esse status pode ser representado por uma enumeração:

public enum VideoStatus
{
    NotReady,
    Playing,
    Paused
}

A Video classe define uma propriedade associável somente leitura chamada Status do tipo VideoStatus. Essa propriedade é definida como somente leitura porque só deve ser definida a partir do manipulador do controle:

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, uma propriedade associável somente leitura terá um acessador set particular na propriedade Status para permitir que ela seja definida na classe. No entanto, para uma View derivada suportada por manipuladores, a propriedade deve ser definida de fora da classe, mas somente pelo manipulador do controle.

Por esse motivo, outra propriedade é definida com o nome IVideoController.Status. Essa é uma implementação explícita da interface e foi possibilitada pela interface IVideoController implementada pela classe Video:

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

Essa interface possibilita que uma classe externa defina Video a Status propriedade referenciando a IVideoController interface. A propriedade pode ser definida a partir de outras classes e do manipulador, mas é improvável que seja definida inadvertidamente. Mais importante ainda, a Status propriedade não pode ser definida por meio de uma associação de dados.

Para ajudar as implementações do manipulador a manter a Status propriedade atualizada, a Video classe define um evento e um UpdateStatus 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));
        }
        ...
    }
}

O OnTimerTick manipulador de eventos é executado a cada décimo de segundo, o que gera o UpdateStatus evento e invoca o UpdateStatus comando.

Quando o UpdateStatus comando é enviado do Video controle para seu manipulador, o mapeador de comandos do manipulador garante que o MapUpdateStatus método seja invocado:

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

O MapUpdateStatus método, por sua vez, chama o UpdateStatus método na propriedade do PlatformView manipulador. A PlatformView propriedade, que é do tipo MauiVideoPlayer, encapsula as exibições nativas que fornecem a implementação do player de vídeo em cada plataforma.

Android

O exemplo de código a seguir mostra que o UpdateStatus método no Android define a Status propriedade:

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;
            ...
        }
        ...
    }
}

A VideoView.IsPlaying propriedade é um booleano que indica se o vídeo está sendo reproduzido ou pausado. Para determinar se o VideoView não pode reproduzir ou pausar o vídeo, seu Prepared evento deve ser manipulado. Esse evento é gerado quando a fonte de mídia está pronta para reprodução. O evento é inscrito no MauiVideoPlayer construtor e cancelado em sua Dispose substituição. Em seguida, o UpdateStatus método usa o isPrepared campo e a VideoView.IsPlaying propriedade para definir a Status propriedade no objeto, convertendo-o Video em IVideoController.

Catalisador para iOS e Mac

O exemplo de código a seguir mostra o UpdateStatus método no iOS e no Mac que o Catalyst define a Status propriedade:

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;
            ...
        }
        ...
    }
}

Duas propriedades de AVPlayer devem ser acessadas para definir a Status propriedade - a Status propriedade do tipo AVPlayerStatus e a TimeControlStatus propriedade do tipo AVPlayerTimeControlStatus. A Status propriedade pode então ser definida no Video objeto convertendo-o em IVideoController.

Windows

O exemplo de código a seguir mostra que o UpdateStatus método no Windows define a Status propriedade:

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;
            }
        }
        ...
    }
}

O UpdateStatus método usa o MediaPlayerElement.MediaPlayer.CurrentState valor da propriedade para determinar o valor da Status propriedade. A Status propriedade pode então ser definida no Video objeto convertendo-o em IVideoController.

Barra de posicionamento

Os controles de transporte implementados por cada plataforma incluem uma barra de posicionamento. Essa barra se assemelha a um controle deslizante ou barra de rolagem e mostra a localização atual do vídeo dentro de sua duração total. Os usuários podem manipular a barra de posicionamento para avançar ou retroceder para uma nova posição no vídeo.

A implementação de sua própria barra de posicionamento exige que a turma Video saiba a duração do vídeo e sua posição atual dentro dessa duração.

Duration

Um item de informação que o Video controle precisa para dar suporte a uma barra de posicionamento personalizada é a duração do vídeo. A classe Video define uma propriedade associável somente leitura chamada Duration, do tipo TimeSpan. Essa propriedade é definida como somente leitura porque só deve ser definida a partir do manipulador do controle:

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, uma propriedade associável somente leitura terá um acessador set particular na propriedade Duration para permitir que ela seja definida na classe. No entanto, para uma View derivada suportada por manipuladores, a propriedade deve ser definida de fora da classe, mas somente pelo manipulador do controle.

Observação

O manipulador de eventos de propriedade alterada para a Duration propriedade associável chama um método chamado SetTimeToEnd, que é descrito em Calculando o tempo de término.

Por esse motivo, outra propriedade é definida com o nome IVideoController.Duration. Essa é uma implementação explícita da interface e foi possibilitada pela interface IVideoController implementada pela classe Video:

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

Essa interface possibilita que uma classe externa defina Video a Duration propriedade referenciando a IVideoController interface. A propriedade pode ser definida a partir de outras classes e do manipulador, mas é improvável que seja definida inadvertidamente. Mais importante ainda, a Duration propriedade não pode ser definida por meio de uma associação de dados.

A duração de um vídeo não está disponível imediatamente após a Source Video definição da propriedade do controle. O vídeo deve ser parcialmente baixado antes que a visualização nativa possa determinar sua duração.

Android

No Android, a propriedade relata VideoView.Duration uma duração válida em milissegundos após o VideoView.Prepared evento ter sido gerado. A MauiVideoPlayer classe usa o manipulador de Prepared eventos para obter o valor da Duration propriedade:

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);
        }
        ...
    }
}
Catalisador para iOS e Mac

No iOS e no Mac Catalyst, a duração de um vídeo é obtida da propriedade, mas não imediatamente após a AVPlayerItem.Duration AVPlayerItem criação. É possível definir um observador do iOS para a Duration propriedade, mas a MauiVideoPlayer classe obtém a duração no UpdateStatus método chamado 10 vezes 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);
                ...
            }
        }
        ...
    }
}

O método ConvertTime converte um objeto CMTime em um TimeSpan valor.

Windows

No Windows, a MediaPlayerElement.MediaPlayer.NaturalDuration propriedade é um TimeSpan valor que se torna válido quando o MediaPlayerElement.MediaPlayer.MediaOpened evento é gerado. A MauiVideoPlayer classe usa o manipulador de MediaOpened eventos para obter o valor da NaturalDuration propriedade:

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;
            });
        }
        ...
    }
}

Em seguida, o OnMediaPlayer manipulador de eventos chama o MainThread.BeginInvokeOnMainThread método para definir a Duration propriedade no Video objeto, convertendo-a em IVideoController, no thread principal. Isso é necessário porque o MediaPlayerElement.MediaPlayer.MediaOpened evento é tratado em um thread em segundo plano. Para obter mais informações sobre como executar código no thread principal, consulte Criar um thread no thread da interface do usuário do .NET MAUI.

Cargo

O Video controle também precisa de uma Position propriedade que aumenta de zero à Duration medida que o vídeo é reproduzido. A Video classe implementa essa propriedade como uma propriedade associável com public get e set acessadores:

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); }
        }
        ...
    }
}

O get acessador retorna a posição atual do vídeo enquanto está sendo reproduzido. O set acessador responde à manipulação do usuário da barra de posicionamento movendo a posição do vídeo para frente ou para trás.

Observação

O manipulador de eventos de propriedade alterada para a Position propriedade associável chama um método chamado SetTimeToEnd, que é descrito em Calculando o tempo de término.

No Android, iOS e Mac Catalyst, a propriedade que obtém a posição atual tem apenas um get acessador. Em vez disso, um Seek método está disponível para definir a posição. Esta parece ser uma abordagem mais sensata do que usar uma única Position propriedade, que tem um problema inerente. À medida que um vídeo é reproduzido, uma Position propriedade deve ser atualizada continuamente para refletir a nova posição. Mas você não quer que a Position maioria das alterações da propriedade faça com que o player de vídeo se mova para uma nova posição no vídeo. Se isso acontecesse, o player de vídeo responderia buscando o último valor da propriedade Position e o vídeo não avançaria.

Apesar das dificuldades de implementar uma Position propriedade com get acessadores e set , essa abordagem é usada porque pode utilizar a associação de dados. A Position propriedade do Video controle pode ser associada a um Slider que é usado para exibir a posição e buscar uma nova posição. No entanto, vários cuidados são necessários ao implementar a Position propriedade, para evitar loops de feedback.

Android

No Android, a VideoView.CurrentPosition propriedade indica a posição atual do vídeo. A MauiVideoPlayer classe define a Position UpdateStatus propriedade no método ao mesmo tempo em que define a Duration propriedade:

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);
            }
        }
        ...
    }
}

Toda vez que a Position propriedade é definida pelo UpdateStatus método, a propriedade dispara um PropertyChanged evento, o Position que faz com que o mapeador de propriedades do manipulador chame o UpdatePosition método. O UpdatePosition método não deve fazer nada para a maioria das alterações de propriedade. Caso contrário, a cada mudança na posição do vídeo, ele seria movido para a mesma posição que acabou de alcançar. Para evitar esse loop de feedback, o UpdatePosition só chama o Seek método no VideoView objeto quando a diferença entre a Position propriedade e a posição atual do é maior que um VideoView segundo.

Catalisador para iOS e Mac

No iOS e no Mac Catalyst, a AVPlayerItem.CurrentTime propriedade indica a posição atual do vídeo. A MauiVideoPlayer classe define a Position UpdateStatus propriedade no método ao mesmo tempo em que define a Duration propriedade:

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));
            }
        }
        ...
    }
}

Toda vez que a Position propriedade é definida pelo UpdateStatus método, a propriedade dispara um PropertyChanged evento, o Position que faz com que o mapeador de propriedades do manipulador chame o UpdatePosition método. O UpdatePosition método não deve fazer nada para a maioria das alterações de propriedade. Caso contrário, a cada mudança na posição do vídeo, ele seria movido para a mesma posição que acabou de alcançar. Para evitar esse loop de feedback, o UpdatePosition só chama o Seek método no AVPlayer objeto quando a diferença entre a Position propriedade e a posição atual do é maior que um AVPlayer segundo.

Windows

No Windows, a MediaPlayerElement.MedaPlayer.Position propriedade indica a posição atual do vídeo. A MauiVideoPlayer classe define a Position UpdateStatus propriedade no método ao mesmo tempo em que define a Duration propriedade:

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;
                }
            }
        }
        ...
    }
}

Toda vez que a Position propriedade é definida pelo UpdateStatus método, a propriedade dispara um PropertyChanged evento, o Position que faz com que o mapeador de propriedades do manipulador chame o UpdatePosition método. O UpdatePosition método não deve fazer nada para a maioria das alterações de propriedade. Caso contrário, a cada mudança na posição do vídeo, ele seria movido para a mesma posição que acabou de alcançar. Para evitar esse loop de feedback, o UpdatePosition só define a MediaPlayerElement.MediaPlayer.Position propriedade quando a diferença entre a Position propriedade e a posição atual do é maior que um MediaPlayerElement segundo.

Calculando o tempo até o fim

Às vezes, os players de vídeo mostram o tempo restante no vídeo. Esse valor começa na duração do vídeo quando o vídeo começa e diminui para zero quando o vídeo termina.

A Video classe inclui uma propriedade somente TimeToEnd leitura que é calculada com base nas alterações nas Duration propriedades and 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;
        }
        ...
    }
}

O SetTimeToEnd método é chamado a partir dos manipuladores de eventos de propriedade alterada das Duration propriedades e Position .

Barra de posicionamento personalizada

Uma barra de posicionamento personalizada pode ser implementada criando uma classe que deriva de Slider, que contém Duration e Position propriedades do 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;
                }
            };
        }
    }
}

O manipulador de eventos de propriedade alterada para a Duration propriedade define a Slider Maximum propriedade do para a TotalSeconds propriedade do TimeSpan valor. Da mesma forma, o manipulador de eventos de propriedade alterada para a Position propriedade define a Value propriedade do Slider. Este é o mecanismo pelo qual o Slider rastreia a posição de PositionSlider.

O PositionSlider é atualizado a partir do subjacente Slider em apenas um cenário, que é quando o usuário manipula o Slider para indicar que o vídeo deve ser avançado ou revertido para uma nova posição. Isso é detectado PropertyChanged no manipulador no PositionSlider construtor. Esse manipulador de eventos verifica se há uma alteração na Value propriedade e, se for diferente da Position propriedade, a Position propriedade será definida a partir da Value propriedade.

Registrar o manipulador

Um controle personalizado e seu manipulador devem ser registrados em um aplicativo antes de serem consumidos. Isso deve ocorrer no método CreateMauiApp na classe MauiProgram em seu projeto de aplicativo, que é o ponto de entrada multiplataforma do aplicativo:

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();
    }
}

O manipulador é registrado com o ConfigureMauiHandlers método and AddHandler . O primeiro argumento para o AddHandler método é o tipo de controle multiplataforma, com o segundo argumento sendo seu tipo de manipulador.

Consumir o controle multiplataforma

Depois de registrar o manipulador com seu aplicativo, o controle multiplataforma pode ser consumido.

Reproduzir um vídeo da Web

O Video controle pode reproduzir um vídeo de uma URL, conforme mostrado no exemplo a seguir:

<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>

Neste exemplo, a classe converte VideoSourceConverter a cadeia de caracteres que representa o URI em um UriVideoSource. O vídeo começa a carregar e a ser reproduzido assim que uma quantidade suficiente de dados for baixada e armazenada em buffer. Em cada plataforma, os controles de transporte desaparecem se não forem usados, mas podem ser restaurados tocando no vídeo.

Reproduzir um recurso de vídeo

Os arquivos de vídeo inseridos na pasta Resources\Raw do aplicativo, com uma ação de build MauiAsset , podem ser reproduzidos pelo Video controle:

<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>

Neste exemplo, a classe converte VideoSourceConverter a cadeia de caracteres que representa o nome do arquivo do vídeo em um ResourceVideoSourcearquivo . Para cada plataforma, o vídeo começa a ser reproduzido quase imediatamente após a definição da fonte de vídeo, pois o arquivo está no pacote do aplicativo e não precisa ser baixado. Em cada plataforma, os controles de transporte desaparecem se não forem usados, mas podem ser restaurados tocando no vídeo.

Reproduzir um arquivo de vídeo da biblioteca do dispositivo

Os arquivos de vídeo armazenados no dispositivo podem ser recuperados e reproduzidos pelo Video controle:

<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>

Quando o Button é tocado, seu Clicked manipulador de eventos é executado, o que é mostrado no exemplo de código a seguir:

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;
}

O Clicked manipulador de eventos usa a classe do .NET MAUI MediaPicker para permitir que o usuário escolha um arquivo de vídeo do dispositivo. O arquivo de vídeo escolhido é encapsulado como um FileVideoSource objeto e definido como a Source propriedade do Video controle. Para obter mais informações sobre a MediaPicker classe, consulte Seletor de mídia. Para cada plataforma, o vídeo começa a ser reproduzido quase que imediatamente após a origem do vídeo ser definida, porque o arquivo está no dispositivo e não precisa ser baixado. Em cada plataforma, os controles de transporte desaparecem se não forem usados, mas podem ser restaurados tocando no vídeo.

Configurar o controle de vídeo

Você pode impedir que um vídeo seja iniciado automaticamente definindo a AutoPlay propriedade como false:

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

Você pode suprimir os controles de transporte definindo a AreTransportControlsEnabled propriedade como false:

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

Se você definir AutoPlay e AreTransportControlsEnabled para false, o vídeo não começará a ser reproduzido e não haverá como iniciá-lo a ser reproduzido. Nesse cenário, você precisaria chamar o Play método do arquivo code-behind ou criar seus próprios controles de transporte.

Além disso, você pode definir um vídeo para loop definindo a IsLooping propriedade como true:

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

Se você definir a IsLooping propriedade como true isso, o controle definirá Video automaticamente a posição do vídeo para o início depois de chegar ao fim.

Usar controles de transporte personalizados

O exemplo XAML a seguir mostra controles de transporte personalizados que reproduzem, pausam e param o vídeo:

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

Neste exemplo, o Video controle define a AreTransportControlsEnabled propriedade e false define um Button que reproduz e pausa o vídeo e um Button que interrompe a reprodução do vídeo. A aparência do botão é definida usando caracteres unicode e seus equivalentes de texto, para criar botões que consistem em um ícone e texto:

Captura de tela dos botões de reprodução e pausa.

Quando o vídeo está sendo reproduzido, o botão de reprodução é atualizado para um botão de pausa:

Captura de tela dos botões de pausa e parada.

A interface do usuário também inclui um ActivityIndicator que é exibido enquanto o vídeo está sendo carregado. Os gatilhos de dados são usados para habilitar e desabilitar os ActivityIndicator botões e os e para alternar o primeiro botão entre reproduzir e pausar. Para obter mais informações sobre gatilhos de dados, consulte Gatilhos de dados.

O arquivo code-behind define os manipuladores de eventos para os eventos de botão Clicked :

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 posicionamento personalizada

O exemplo a seguir mostra uma barra de posicionamento personalizada, PositionSlider, sendo consumida em 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>

A Position propriedade do Video objeto está vinculada à Position propriedade do , sem problemas de PositionSliderdesempenho, porque a Video.Position propriedade é alterada pelo MauiVideoPlayer.UpdateStatus método em cada plataforma, que é chamado apenas 10 vezes por segundo. Além disso, dois Label objetos exibem os Position valores de propriedades e TimeToEnd do Video objeto.

Limpeza de exibição nativa

A implementação do manipulador de cada plataforma substitui a implementação, que é usada para executar a DisconnectHandler limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. No entanto, essa substituição não é invocada intencionalmente pelo .NET MAUI. Em vez disso, você deve invocá-lo por conta própria de um local adequado no ciclo de vida do seu aplicativo. Isso geralmente ocorrerá quando a página que contém o Video controle for navegada para fora, o que faz com que o evento da Unloaded página seja gerado.

Um manipulador de eventos para o evento da Unloaded página pode ser registrado em XAML:

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

O manipulador de eventos do Unloaded evento pode invocar o DisconnectHandler método em sua Handler instância:

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

Além de limpar os recursos de exibição nativos, invocar o método do DisconnectHandler manipulador também garante que os vídeos parem de ser reproduzidos na navegação regressiva no iOS.

Desconexão do manipulador de controle

A implementação do manipulador de cada plataforma substitui a implementação, que é usada para executar a DisconnectHandler limpeza de exibição nativa, como cancelar a assinatura de eventos e descartar objetos. Por padrão, os manipuladores se desconectam automaticamente de seus controles quando possível, como ao navegar para trás em um aplicativo.

Em alguns cenários, talvez você queira controlar quando um manipulador se desconecta de seu controle, o que pode ser obtido com a HandlerProperties.DisconnectPolicy propriedade anexada. Essa propriedade requer um HandlerDisconnectPolicy argumento, com a enumeração definindo os seguintes valores:

  • Automatic, o que indica que o manipulador será desconectado automaticamente. Esse é o valor padrão da propriedade anexada HandlerProperties.DisconnectPolicy.
  • Manual, o que indica que o manipulador terá que ser desconectado manualmente invocando a DisconnectHandler() implementação.

O exemplo a seguir mostra a configuração da propriedade anexada HandlerProperties.DisconnectPolicy:

<controls:Video x:Name="video"
                HandlerProperties.DisconnectPolicy="Manual"
                Source="video.mp4"
                AutoPlay="False" />

Este é o código C# equivalente:

Video video = new Video
{
    Source = "video.mp4",
    AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video, HandlerDisconnectPolicy.Manual);

Ao definir a propriedade anexada HandlerProperties.DisconnectPolicy como Manual , você deve invocar a implementação do DisconnectHandler manipulador por conta própria de um local adequado no ciclo de vida do aplicativo. Isso pode ser feito invocando video.Handler?.DisconnectHandler();.

Além disso, há um método de extensão DisconnectHandlers que desconecta os manipuladores de um determinado IView:

video.DisconnectHandlers();

Ao se desconectar, o método DisconnectHandlers se propagará para baixo na árvore de controle até que seja concluído ou chegue a um controle que tenha definido uma política manual.