Partilhar via


Criar um controle personalizado usando manipuladores

Browse sample. Navegue pelo exemplo

Um requisito padrão para aplicativos é a capacidade de reproduzir vídeos. Este artigo examina como criar um controle de plataforma cruzada Video .NET Multi-platform App UI (.NET MAUI) que usa um manipulador para mapear a API de controle entre plataformas 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 incorporado 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:

Screenshot of video playback on iOS.Screenshot of video playback using custom transport controls on 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:

Video handler architecture.

A Video classe fornece a API de plataforma cruzada para o controle. O mapeamento da API de plataforma cruzada para as APIs de exibição nativas é executado pela classe em cada plataforma, que mapeia VideoHandler a classe para a VideoMauiVideoPlayer 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 desacopla seus manipuladores de seus controles entre plataformas por meio de interfaces. Isso permite que estruturas experimentais como Comet e Fabulous forneçam seus próprios controles de plataforma cruzada, que implementam as interfaces, enquanto ainda usam os manipuladores do .NET MAUI. A criação de uma interface para seu controle de plataforma cruzada só é necessária se você precisar desacoplar seu manipulador de seu controle de plataforma cruzada para uma finalidade semelhante ou para fins de teste.

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

  1. Crie uma classe para o controle de plataforma cruzada, que fornece a API pública do controle. Para obter mais informações, consulte Criar o controle entre plataformas.
  2. Crie quaisquer tipos adicionais de plataforma cruzada necessários.
  3. Crie uma partial classe de manipulador. Para obter mais informações, consulte Criar o manipulador.
  4. Na classe handler, 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 handler, crie um CommandMapper dicionário, que define as Ações a serem executadas quando o controle entre plataformas envia instruções para as exibições nativas que implementam o controle entre plataformas. Para obter mais informações, consulte Criar o mapeador de comandos.
  6. Crie partial classes de manipulador para cada plataforma que cria as exibições nativas que implementam o controle entre plataformas. Para obter mais informações, consulte Criar os controles de plataforma.
  7. Registre o manipulador usando os ConfigureMauiHandlers métodos e AddHandler na classe do MauiProgram seu 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 entre plataformas.

Criar o controle multiplataforma

Para criar um controle entre plataformas, você deve criar uma classe que deriva 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 entre plataformas 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 de plataforma cruzada, 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 define PlatformView como sendo 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 PlatformView propriedade uma vez por plataforma, usando compilação condicional.

Criar o mapeador de propriedades

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

PropertyMapper é definido na classe genérica ViewHandler do .NET MAUI e requer que dois argumentos genéricos sejam fornecidos:

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

O exemplo de código a seguir mostra a classe estendida com a VideoHandlerPropertyMapper 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 cuja chave é um e cujo valor é um Dictionarystring genérico Action. O string representa o nome da propriedade do controle entre plataformas e o representa um static método que requer o manipulador e o Action controle entre plataformas 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 nativa. Isso garante que, quando uma propriedade é definida em um controle entre plataformas, a exibição nativa subjacente será atualizada conforme necessário. A vantagem dessa abordagem é que ela permite a fácil personalização de controle entre plataformas, pois o mapeador de propriedades pode ser modificado por consumidores de controle entre plataformas sem subclassificação.

Criar o mapeador de comandos

Cada manipulador também pode fornecer um mapeador de comandos, que define quais Ações devem ser executadas quando o controle entre plataformas 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 que mapeia Dictionary membros de controle de plataforma cruzada para suas Ações associadas.

CommandMapper é definido na classe genérica ViewHandler do .NET MAUI e requer que dois argumentos genéricos sejam fornecidos:

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

O exemplo de código a seguir mostra a classe estendida com a VideoHandlerCommandMapper 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 cuja chave é um e cujo valor é um Dictionarystring genérico Action. O string representa o nome do comando do controle de plataforma cruzada e o representa um static método que requer o manipulador, o Action controle entre plataformas e 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 nativa. Isso garante que, quando um comando é enviado do controle entre plataformas, 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, pois o mapeador de comandos pode ser modificado por consumidores de controle multiplataforma sem subclassificação.

Criar os controles da plataforma

Depois de criar os mapeadores para seu 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 oferecer suporte a multidirecionamento baseado em nome de arquivo, multidirecionamento baseado em pasta ou ambos.

O aplicativo de exemplo é configurado para oferecer suporte a multidirecionamento baseado em nome de arquivo, de modo que as classes do manipulador estejam localizadas em uma única pasta:

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

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 arquivos. Essa multisegmentação 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 a multisegmentação, consulte Configurar a multisegmentação.

Cada classe de manipulador de plataforma deve ser uma classe parcial e derivar da classe genérica ViewHandler , que requer dois argumentos de tipo:

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

Importante

A ViewHandler classe fornece VirtualView e PlatformView propriedades. A VirtualView propriedade é usada para acessar o controle de plataforma cruzada de seu manipulador. A PlatformView propriedade, é usada para acessar a exibição nativa em cada plataforma que implementa o controle entre plataformas.

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 entre plataformas.
  • ConnectHandler, que deve executar qualquer configuração de modo de exibição nativo, como inicializar o modo de exibição nativo 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 é intencionalmente invocado pelo .NET MAUI. Em vez disso, você mesmo deve invocá-lo a partir de um local adequado no ciclo de vida do seu aplicativo. Para obter mais informações, consulte Limpeza do modo de exibição nativo.

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 de plataforma cruzada na plataforma. Alternativamente, isso pode ser fornecido por um tipo adicional, que é a abordagem adotada aqui.

Android

O vídeo é reproduzido no Android com um VideoViewarquivo . 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 parcial 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 ViewHandler da classe, com o argumento genérico Video especificando o tipo de controle entre plataformas e o argumento especificando o tipo que encapsula o MauiVideoPlayerVideoView modo de exibição nativo.

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 Action é executada em resposta a uma alteração de propriedade no controle entre plataformas e é um static método que requer instâncias de controle entre plataformas 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 Action é executada em resposta a um comando que está sendo enviado do controle entre plataformas e é um static método que requer instâncias de controle entre plataformas e manipuladores e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, depois de extrair os dados opcionais.

No Android, a classe encapsula o VideoView para manter a MauiVideoPlayer exibiçã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 exibição nativa em alguns cenários.

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

A construtora também se inscreve no VideoView.Prepared evento. Esse evento é gerado quando o vídeo está pronto para reprodução e é cancelado 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 PreparedDispose 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 propriedade estiver definida como , a Video.AreTransportControlsEnabledMediaController será definida como trueo media player do VideoView. Isso ocorre porque quando a AreTransportControlsEnabled propriedade é definida, o mapeador de propriedades do manipulador garante que o método seja chamado, o que, por sua vez, chama o MapAreTransportControlsEnabledUpdateTransportControlsEnabled 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 estiver definida como , o será removido como falseo MediaController media player 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.

iOS e Mac Catalyst

O vídeo é reproduzido no iOS e Mac Catalyst com um e um AVPlayerAVPlayerViewControllerarquivo . 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 ViewHandler da classe, com o argumento genérico Video especificando o tipo de controle entre plataformas e o argumento especificando o MauiVideoPlayer tipo que encapsula as AVPlayer exibições nativas 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 Action é executada em resposta a uma alteração de propriedade no controle entre plataformas e é um static método que requer instâncias de controle entre plataformas 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 Action é executada em resposta a um comando que está sendo enviado do controle entre plataformas e é um static método que requer instâncias de controle entre plataformas e manipuladores e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, depois de extrair os dados opcionais.

No iOS e no Mac Catalyst, a MauiVideoPlayer classe encapsula os AVPlayer tipos e 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 manipulam a interação do usuário com esse conteúdo. O construtor cria um objeto, que gerencia a reprodução e o tempo de um arquivo de mídia e o define como o Player valor da propriedade de um AVPlayerViewControllerAVPlayer arquivo . O AVPlayerViewController exibe o AVPlayer conteúdo do e apresenta controles de transporte e outros recursos. O tamanho e a localização do controle são então definidos, o que garante que o vídeo esteja centralizado na página e se expanda para preencher o espaço disponível, mantendo sua 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. O modo de exibição nativo, que é o modo de exibição do AVPlayerViewController, é então adicionado à página.

O Dispose método é responsável por executar a limpeza da 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 parar 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 de 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 método seja chamado, o que, por sua vez, chama o MapAreTransportControlsEnabledUpdateTransportControlsEnabled 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 mostra 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 do 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 ViewHandler da classe, com o argumento genérico Video especificando o tipo de controle entre plataformas e o argumento especificando o tipo que encapsula o MauiVideoPlayerMediaPlayerElement modo de exibição nativo.

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 Action é executada em resposta a uma alteração de propriedade no controle entre plataformas e é um static método que requer instâncias de controle entre plataformas 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 Action é executada em resposta a um comando que está sendo enviado do controle entre plataformas e é um static método que requer instâncias de controle entre plataformas e manipuladores e dados opcionais como argumentos. Em cada caso, a Action chama um método definido na MauiVideoPlayer classe, depois de extrair os dados opcionais.

No Windows, a MauiVideoPlayer classe encapsula o para manter o MediaPlayerElement modo de exibição nativo separado 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 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 da 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 MediaOpenedDispose limpeza de exibição nativa.

Observação

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

Os controles de transporte de 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 método seja chamado, o que, por sua vez, chama o MapAreTransportControlsEnabledUpdateTransportControlsEnabled 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 mostra 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 de plataforma cruzada .NET MAUI, derivado do , pode ser convertido em seu controle de plataforma subjacente com o método de ElementToPlatform extensã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 ToPlatform método está no Microsoft.Maui.Platform namespace.

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

O ToPlatform método pode converter um controle de plataforma cruzada para seu controle de plataforma subjacente a partir 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 MapSource método converte a Video instância em um MauiVideoPlayer objeto.

O ToPlatform método também pode converter um controle de plataforma cruzada para seu controle de plataforma subjacente a partir de código de plataforma cruzada:

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 de plataforma cruzada nomeado video é convertido em sua exibição nativa subjacente em cada plataforma Video na OnHandlerChanged() substituição. Essa substituição é chamada quando a exibição nativa que implementa o controle entre plataformas está disponível e inicializada. O objeto retornado pelo ToPlatform método pode ser convertido em seu tipo nativo exato, que aqui é um MauiVideoPlayerarquivo .

Reproduzir o vídeo

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

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 é chamado 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 UriVideoSourcearquivo . Caso contrário, ele retorna um ResourceVideoSourcearquivo .

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

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 VideoViewarquivo . O exemplo de código a seguir mostra como o método processa a UpdateSourceSource propriedade quando é 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 método de VideoView é usado para especificar o SetVideoUri 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.

iOS e Mac Catalyst

Para reproduzir um vídeo no iOS e no Mac Catalyst, um objeto do tipo AVAsset é criado para encapsular o vídeo e que é 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 a UpdateSourceSource propriedade quando é 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 videoclasses 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 de vídeo é navegada. Para parar o vídeo, o ReplaceCurrentItemWithPlayerItem está 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 MediaPlayerElementarquivo . O exemplo de código a seguir mostra como o método processa a UpdateSourceSource propriedade quando é 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 propriedade é definida como um objeto que inicializa um UriMediaSource com o URI do vídeo a MediaPlayerElement.Source ser reproduzido. Quando o foi definido, o MediaPlayerElement.Source método do manipulador de OnMediaPlayerMediaOpened eventos é registrado em relação ao MediaPlayerElement.MediaPlayer.MediaOpened evento. Esse manipulador de eventos é usado para definir a Duration propriedade do Video controle.

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

Reproduzir um recurso de vídeo

A ResourceVideoSource classe é usada para acessar arquivos de vídeo que são 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 chamado:

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 a UpdateSourceSource propriedade quando é 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 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 SetVideoPath 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;
        }
        ...
    }
}

iOS e Mac Catalyst

O exemplo de código a seguir mostra como o método processa a UpdateSourceSource propriedade quando é 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 método de NSBundle é usado para recuperar o GetUrlForResource 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 de vídeo é navegada. Para parar o vídeo, o ReplaceCurrentItemWithPlayerItem está 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 a UpdateSourceSource propriedade quando é 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 objeto que inicializa um UriMediaSource com o 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 chamado:

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 a UpdateSourceSource propriedade quando é 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 método de é usado para especificar o SetVideoPath arquivo de VideoView vídeo a ser reproduzido.

iOS e Mac Catalyst

O exemplo de código a seguir mostra como o método processa a UpdateSourceSource propriedade quando é 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 a UpdateSourceSource propriedade quando é 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 método retorna um MediaSource objeto que é definido como o MediaSource.CreateFromStorageFile valor da MediaPlayerElement.Source propriedade.

Fazer um loop de 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 chamado:

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 habilita 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 looping de vídeo, a classe implementa a MauiVideoPlayerMediaPlayer.IOnPreparedListener interface. Essa interface define um OnPrepared retorno de chamada que é chamado quando a fonte de mídia está pronta para reprodução. Quando a Video.IsLooping propriedade é true, o método define MauiVideoPlayer como o objeto que fornece o UpdateIsLooping retorno de OnPrepared chamada. O retorno de chamada define a MediaPlayer.IsLooping propriedade como o valor da Video.IsLooping propriedade.

iOS e Mac Catalyst

O exemplo de código a seguir mostra como o método no iOS e Mac Catalyst habilita 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 no Mac Catalyst, uma notificação é usada para executar um retorno de chamada quando o vídeo é reproduzido até o final. Quando a propriedade é , o método adiciona um observador para a notificação e executa o PlayedToEndUpdateIsLooping método quando a Video.IsLoopingAVPlayerItem.DidPlayToEndTimeNotification notificação é truerecebida. 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 pausará no final da reprodução.

Como MauiVideoPlayer adiciona um observador para uma notificação, ele também deve remover o observador ao executar a limpeza da exibição nativa. Isso é realizado 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 método que remove o observador para a AVPlayerItem.DidPlayToEndTimeNotification notificação e que também invoca o DestroyPlayedToEndObserverDispose método no NSObject.

Windows

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

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

Para habilitar o looping de vídeo, o método define a MediaPlayerElement.MediaPlayer.IsLoopingEnabled propriedade como o UpdateIsLooping 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 são frequentemente identificados com ícones familiares em vez de texto, e os botões de reprodução e pausa são frequentemente combinados em um botão.

Por padrão, o Video controle exibe 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 parar o vídeo e saber o status atual da reprodução de vídeo. A Video classe define métodos chamados Play, Pausee que geram um evento correspondente e Stop 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 , Pausee envia um comando e Stop dados associados ao VideoHandler. O CommandMapper comando for VideoHandler mapeia nomes 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, pois o mapeador de comandos pode ser modificado por consumidores de controle multiplataforma sem subclassificação. 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, e métodos que são executados em resposta ao Video controle de envio PlayRequested, PauseRequestedPauseRequestede StopRequestedStopRequested comandos. Cada método invoca um método em seu modo de exibição nativo para reproduzir, pausar ou parar o vídeo. Por exemplo, o código a seguir mostra os PlayRequestedmétodos , PauseRequestede no iOS e StopRequested no 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 método , ou é chamado no Video controle, sua exibição nativa é instruída a reproduzir, Pausepausar ou parar o vídeo e registrar a posição em que o Playvídeo foi reproduzido, pausado ou Stop parado. 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 oferecer 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 vinculá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 Video defina a Status propriedade fazendo referência à IVideoController interface. A propriedade pode ser definida a partir de outras classes e do manipulador, mas é improvável que seja definida inadvertidamente. Mais importante, 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 propriedade atualizada, a StatusVideo 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 evento e invoca o UpdateStatusUpdateStatus comando.

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

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 o método no Android define a UpdateStatusStatus 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 vídeo não pode ser reproduzido ou pausado, seu Prepared evento deve ser manipulado. Esse evento é gerado quando a fonte de mídia está pronta para reprodução. O evento é inscrito MauiVideoPlayer no construtor e cancelado em sua Dispose substituição. Em UpdateStatus seguida, o método usa o isPrepared campo e a propriedade para definir a Status propriedade no objeto convertendo-a VideoView.IsPlayingVideo em IVideoController.

iOS e Mac Catalyst

O exemplo de código a seguir mostra o método no iOS e Mac Catalyst define a UpdateStatusStatus 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 devem ser acessadas para definir a propriedade - a propriedade de type e a StatusStatusTimeControlStatus propriedade de AVPlayer type .AVPlayerStatusAVPlayerTimeControlStatus A Status propriedade pode então ser definida no objeto convertendo-o Video em IVideoController.

Windows

O exemplo de código a seguir mostra o método no Windows define a UpdateStatusStatus 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 objeto convertendo-o Video 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.

Implementar sua própria barra de posicionamento requer que a classe saiba a Video 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 oferecer suporte a uma barra de posicionamento personalizada é a duração do vídeo. A Video classe define uma propriedade vinculá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 bindable chama um método chamado SetTimeToEnd, que é descrito em Calculando o tempo até o fim.

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 Video defina a Duration propriedade fazendo referência à IVideoController interface. A propriedade pode ser definida a partir de outras classes e do manipulador, mas é improvável que seja definida inadvertidamente. Mais importante, 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 SourceVideo propriedade do controle ser definida. O vídeo deve ser parcialmente baixado antes que a exibição nativa possa determinar sua duração.

Android

No Android, a VideoView.Duration propriedade relata uma duração válida em milissegundos após o VideoView.Prepared evento ter sido gerado. A MauiVideoPlayer classe usa o manipulador de eventos para obter o Prepared 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);
        }
        ...
    }
}
iOS e Mac Catalyst

No iOS e no Mac Catalyst, a duração de um vídeo é obtida da AVPlayerItem.Duration propriedade, mas não imediatamente após a AVPlayerItem criação. É possível definir um observador do iOS para a propriedade, mas a classe obtém a DurationMauiVideoPlayer 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 eventos para obter o MediaOpened 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 OnMediaPlayer seguida, o manipulador de eventos chama o MainThread.BeginInvokeOnMainThread método para definir a Duration propriedade no objeto, convertendo-a Video em IVideoController, no thread principal. Isso é necessário porque o evento é manipulado MediaPlayerElement.MediaPlayer.MediaOpened 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 .NET maui UI.

Position

O Video controle também precisa de uma Position propriedade que aumenta de zero para Duration à medida que o vídeo é reproduzido. A Video classe implementa essa propriedade como uma propriedade vinculável com público 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 como sua reprodução. 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 bindable chama um método chamado SetTimeToEnd, que é descrito em Calculando o tempo até o fim.

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 e set acessadores, essa abordagem é usada porque pode utilizar a vinculação de dados. A Position propriedade do Video controle pode ser vinculada a um Slider que é usado tanto para exibir a posição quanto para buscar uma nova posição. No entanto, vários cuidados são necessários ao implementar a Position propriedade, para evitar ciclos de feedback.

Android

No Android, a propriedade indica a VideoView.CurrentPosition posição atual do vídeo. A MauiVideoPlayer classe define a PositionUpdateStatus 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 propriedade é definida pelo UpdateStatus método, a Position propriedade dispara um PropertyChanged evento, o que faz com que o mapeador de propriedades do manipulador chame o PositionUpdatePosition 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 somente chama o UpdatePositionSeek método no VideoView objeto quando a diferença entre a propriedade e a Position posição atual do VideoView é maior que um segundo.

iOS e Mac Catalyst

No iOS e no Mac Catalyst, a AVPlayerItem.CurrentTime propriedade indica a posição atual do vídeo. A MauiVideoPlayer classe define a PositionUpdateStatus 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 propriedade é definida pelo UpdateStatus método, a Position propriedade dispara um PropertyChanged evento, o que faz com que o mapeador de propriedades do manipulador chame o PositionUpdatePosition 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 somente chama o UpdatePositionSeek método no AVPlayer objeto quando a diferença entre a propriedade e a Position posição atual do AVPlayer é maior que um segundo.

Windows

No Windows, a propriedade indica a MediaPlayerElement.MedaPlayer.Position posição atual do vídeo. A MauiVideoPlayer classe define a PositionUpdateStatus 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 propriedade é definida pelo UpdateStatus método, a Position propriedade dispara um PropertyChanged evento, o que faz com que o mapeador de propriedades do manipulador chame o PositionUpdatePosition 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 somente define a propriedade quando a diferença entre a propriedade e a MediaPlayerElement.MediaPlayer.PositionPosition posição atual do MediaPlayerElement é maior que um 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 e 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 propriedade define a SliderMaximum propriedade do para a TotalSecondsDuration propriedade do TimeSpan valor. Da mesma forma, o manipulador de eventos de propriedade alterada para a propriedade define a PositionValue 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 para indicar que o Slider vídeo deve ser avançado ou revertido para uma nova posição. Isso é detectado no manipulador PropertyChanged no PositionSlider construtor. Esse manipulador de eventos verifica se há uma alteração na Value propriedade e, se for diferente da propriedade, a Position propriedade será definida a Value partir da Position propriedade.

Registrar o manipulador

Um controle personalizado e seu manipulador devem ser registrados com um aplicativo, antes de poderem ser consumidos. Isso deve ocorrer no CreateMauiApp método na MauiProgram classe em seu projeto de aplicativo, que é o ponto de entrada entre plataformas para o 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 e AddHandler . O primeiro argumento para o método é o tipo de controle entre plataformas, com o AddHandler segundo argumento sendo seu tipo de manipulador.

Consuma o controle multiplataforma

Depois de registrar o manipulador com seu aplicativo, o controle entre plataformas 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 a VideoSourceConverter cadeia de caracteres que representa o URI em um UriVideoSourcearquivo . O vídeo, em seguida, começa a carregar e começa a ser reproduzido uma vez que uma quantidade suficiente de dados foi 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 incorporados na pasta Resources\Raw do aplicativo, com uma ação de compilação 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 a VideoSourceConverter 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 fonte do vídeo ser definida, porque 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, em seguida, 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 é tocado, seu Clicked manipulador de eventos é executado, o Button 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 para permitir que o usuário escolha um arquivo de vídeo do MediaPicker dispositivo. O arquivo de vídeo selecionado é 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 Video

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 para false, o vídeo não começará a ser reproduzido e AreTransportControlsEnabled não haverá como iniciá-lo a reproduzir. 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 como 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 propriedade para isso, garantirá que o controle defina automaticamente a IsLooping posição do vídeo para true o Video início depois de atingir seu 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 controle define a propriedade como false e define um que reproduz e pausa o Video vídeo e um ButtonButton que interrompe a AreTransportControlsEnabled reprodução de 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:

Screenshot of play and pause buttons.

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

Screenshot of pause and stop buttons.

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 ativar e desativar os ActivityIndicator botões 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 objeto é vinculada à Position propriedade do Video , 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 e TimeToEnd properties do Video objeto.

Limpeza do modo de exibição nativo

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 é intencionalmente invocada pelo .NET MAUI. Em vez disso, você mesmo deve invocá-lo a partir de um local adequado no ciclo de vida do seu aplicativo. Isso geralmente ocorre quando a página que contém o controle é navegada para longe, o que faz com que o Video 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 para o evento pode invocar o UnloadedDisconnectHandler método em sua Handler instância:

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

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