Поделиться через


Создание пользовательского элемента управления с помощью обработчиков

Просмотрите пример. Обзор примера

Стандартное требование для приложений — это возможность воспроизведения видео. В этой статье рассматривается создание кроссплатформенного Video пользовательского интерфейса приложений .NET (.NET MAUI), использующего обработчик для сопоставления кроссплатформенного API управления с собственными представлениями в Android, iOS и Mac Catalyst, которые играют видео. Этот элемент управления может воспроизводить видео из трех источников:

  • URL-адрес, представляющий удаленное видео.
  • Ресурс, который является файлом, внедренным в приложение.
  • Файл из видеотеки устройства.

Для управления видео требуются элементы управления транспортом, которые являются кнопками воспроизведения и приостановки видео, а также панель размещения, которая показывает ход выполнения видео и позволяет пользователю быстро перемещаться в другое расположение. Элемент Video управления может использовать элементы управления транспортом и позиционирование панели, предоставляемые платформой, или вы можете предоставить настраиваемые элементы управления транспортом и позиционирование панели. На следующих снимках экрана показан элемент управления в iOS и без пользовательских элементов управления транспортом:

Снимок экрана: воспроизведение видео в iOS.Снимок экрана: воспроизведение видео с помощью пользовательских элементов управления транспортировкой в iOS.

Более сложный элемент управления видео будет иметь дополнительные функции, такие как управление громкостью, механизм прерывания воспроизведения видео при получении вызова и способ поддержания активности экрана во время воспроизведения.

Архитектура элемента управления показана на следующей Video схеме:

Архитектура обработчика видео.

Класс Video предоставляет кроссплатформенный API для элемента управления. Сопоставление кроссплатформенного API с API собственного представления выполняется классом VideoHandler на каждой платформе, который сопоставляет Video класс с классом MauiVideoPlayer . В iOS и Mac Catalyst MauiVideoPlayer класс использует AVPlayer тип для воспроизведения видео. В Android MauiVideoPlayer класс использует VideoView тип для воспроизведения видео. В MauiVideoPlayer Windows класс использует MediaPlayerElement тип для воспроизведения видео.

Внимание

.NET MAUI отделяет обработчики от кроссплатформенных элементов управления через интерфейсы. Это позволяет экспериментальным платформам, таким как Comet и Fabulous, предоставлять собственные кроссплатформенные элементы управления, реализующие интерфейсы, а также использовать обработчики .NET MAUI. Создание интерфейса для кроссплатформенного элемента управления необходимо только в том случае, если необходимо отделить обработчик от кроссплатформенного элемента управления для аналогичной цели или для тестирования.

Процесс создания кроссплатформенного пользовательского элемента управления .NET MAUI, реализация платформы которого предоставляется обработчиками, выглядит следующим образом:

  1. Создайте класс для кроссплатформенного элемента управления, который предоставляет общедоступный API элемента управления. Дополнительные сведения см. в разделе "Создание кроссплатформенного элемента управления".
  2. Создайте все необходимые кроссплатформенные типы.
  3. Создайте класс обработчика partial . Дополнительные сведения см. в разделе "Создание обработчика".
  4. В классе обработчика создайте PropertyMapper словарь, определяющий действия, которые необходимо предпринять при изменении кроссплатформенного свойства. Дополнительные сведения см. в разделе "Создание схемы свойств".
  5. При необходимости в классе обработчика создайте CommandMapper словарь, который определяет действия, которые необходимо предпринять, когда кроссплатформенный элемент управления отправляет инструкции в собственные представления, реализующие кроссплатформенный элемент управления. Дополнительные сведения см. в статье "Создание схемы команд".
  6. Создайте partial классы обработчика для каждой платформы, создающей собственные представления, реализующие кроссплатформенный элемент управления. Дополнительные сведения см. в разделе "Создание элементов управления платформы".
  7. Зарегистрируйте обработчик с помощью ConfigureMauiHandlers методов AddHandler и методов в классе приложения MauiProgram . Дополнительные сведения см. в разделе "Регистрация обработчика".

Затем можно использовать кроссплатформенный элемент управления. Дополнительные сведения см. в разделе "Использование кроссплатформенного элемента управления".

Создание кроссплатформенного элемента управления

Чтобы создать кроссплатформенный элемент управления, необходимо создать класс, производный от 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); }
        }        
        ...
    }
}

Элемент управления должен предоставить общедоступный API, к которому будет обращаться его обработчик, и управлять потребителями. Кроссплатформенные элементы управления должны быть производными от Viewэлемента, представляющего визуальный элемент, используемый для размещения макетов и представлений на экране.

Создание обработчика

После создания кроссплатформенного элемента управления необходимо создать partial класс для обработчика:

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

Класс обработчика — это частичный класс, реализация которого будет завершена на каждой платформе с дополнительным частичным классом.

using Условные операторы определяют PlatformView тип на каждой платформе. В Android, iOS, Mac Catalyst и Windows собственные представления предоставляются пользовательским MauiVideoPlayer классом. Окончательный условный using оператор определяет PlatformView , равный System.Object. Это необходимо, чтобы PlatformView тип можно было использовать в обработчике для использования на всех платформах. Альтернативой будет определение PlatformView свойства один раз на каждую платформу с помощью условной компиляции.

Создание схемы свойств

Каждый обработчик обычно предоставляет средство сопоставления свойств, которое определяет действия, которые необходимо предпринять при изменении свойства в кроссплатформенной элементе управления. Тип PropertyMapper — это Dictionary тип, который сопоставляет свойства кроссплатформенного элемента управления с связанными действиями.

PropertyMapper определяется в классе .NET MAUI ViewHandler<TVirtualView,TPlatformView> и требуется предоставить два универсальных аргумента:

  • Класс для кроссплатформенного элемента управления, производный от View.
  • Класс обработчика.

В следующем примере кода показан класс, расширенный VideoHandler с определением PropertyMapper :

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

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

Dictionary Это PropertyMapper ключ, ключ которого является string универсальнымAction. Представляет string имя свойства кроссплатформенного элемента управления и Action представляет static метод, который требует обработчика и кроссплатформенного элемента управления в качестве аргументов. Например, сигнатурой MapSource метода является public static void MapSource(VideoHandler handler, Video video).

Каждый обработчик платформы должен предоставлять реализации действий, которые управляют API собственного представления. Это гарантирует, что при установке свойства на кроссплатформенный элемент управления базовое собственное представление будет обновлено по мере необходимости. Преимущество этого подхода заключается в том, что это позволяет легко настраивать кроссплатформенный элемент управления, так как средство сопоставления свойств может быть изменено потребителями кроссплатформенных элементов управления без подклассов.

Создание схемы команд

Каждый обработчик также может предоставить средство сопоставления команд, определяющее, какие действия следует предпринять, когда кроссплатформенный элемент управления отправляет команды в собственные представления. Карты команд похожи на схемы свойств, но позволяют передавать дополнительные данные. В этом контексте команда представляет собой инструкцию и, при необходимости, ее данные, которые отправляются в собственное представление. Тип CommandMapper — это Dictionary сопоставление элементов управления между платформами с связанными действиями.

CommandMapper определяется в классе .NET MAUI ViewHandler<TVirtualView,TPlatformView> и требуется предоставить два универсальных аргумента:

  • Класс для кроссплатформенного элемента управления, производный от View.
  • Класс обработчика.

В следующем примере кода показан класс, расширенный VideoHandler с определением CommandMapper :

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

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

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

Dictionary Это CommandMapper ключ, ключ которого является string универсальнымAction. Представляет string имя команды кроссплатформенного элемента управления и Action представляет static метод, который требует обработчика, кроссплатформенного элемента управления и необязательных данных в качестве аргументов. Например, сигнатурой MapPlayRequested метода является public static void MapPlayRequested(VideoHandler handler, Video video, object? args).

Каждый обработчик платформы должен предоставлять реализации действий, которые управляют API собственного представления. Это гарантирует, что при отправке команды из кроссплатформенного элемента управления базовый собственный вид будет управляться по мере необходимости. Преимущество этого подхода заключается в том, что он удаляет необходимость в собственных представлениях для подписки на события кроссплатформенного элемента управления и отмены подписки. Кроме того, это позволяет легко настраивать, так как средство сопоставления команд может быть изменено потребителями кроссплатформенного элемента управления без подклассов.

Создание элементов управления платформы

После создания карт для обработчика необходимо предоставить реализации обработчика на всех платформах. Это можно сделать, добавив реализации обработчика частичного класса в дочерние папки папки Platform . Кроме того, вы можете настроить проект для поддержки многонацеливания на основе файлов или многонацеливания на основе папок или обоих.

Пример приложения настроен для поддержки многонацеливания на основе файлов, чтобы классы обработчиков находились в одной папке:

Снимок экрана: файлы в папке Handlers проекта.

Класс VideoHandler , содержащий mappers, называется VideoHandler.cs. Его реализации платформы находятся в файлах VideoHandler.Android.cs, VideoHandler.MaciOS.cs и VideoHandler.Windows.cs . Этот многонацелевой набор файлов настраивается путем добавления следующего XML-файла в файл проекта в качестве дочерних <Project> элементов узла:

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

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

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

Дополнительные сведения о настройке нескольких целевых моделей см. в разделе "Настройка многонацеливания".

Каждый класс обработчика платформы должен быть частичным и производным от ViewHandler<TVirtualView,TPlatformView> класса, для которого требуется два аргумента типа:

  • Класс для кроссплатформенного элемента управления, производный от View.
  • Тип собственного представления, реализующего кроссплатформенный элемент управления на платформе. Это должно быть идентично типу PlatformView свойства в обработчике.

Внимание

Класс ViewHandler<TVirtualView,TPlatformView> предоставляет VirtualView и PlatformView свойства. Свойство VirtualView используется для доступа к кроссплатформенным элементу управления из обработчика. Свойство PlatformView используется для доступа к собственному представлению на каждой платформе, реализующей кроссплатформенный элемент управления.

Каждая из реализаций обработчика платформы должна переопределить следующие методы:

  • CreatePlatformView, который должен создавать и возвращать собственное представление, реализующее кроссплатформенный элемент управления.
  • ConnectHandler, который должен выполнять любую настройку собственного представления, например инициализацию собственного представления и выполнение подписок на события.
  • DisconnectHandler, который должен выполнять очистку собственного представления, например отмену подписки на события и удаление объектов.

Внимание

Метод DisconnectHandler намеренно не вызывается .NET MAUI. Вместо этого необходимо вызвать его самостоятельно из подходящего расположения в жизненном цикле приложения. Дополнительные сведения см. в разделе "Очистка собственного представления".

Внимание

Метод DisconnectHandler автоматически вызывается .NET MAUI по умолчанию, хотя это поведение может быть изменено. Дополнительные сведения см. в разделе "Отключение обработчика элементов управления".

Каждый обработчик платформы также должен реализовывать действия, определенные в словарях mapper.

Кроме того, каждый обработчик платформы также должен предоставлять код, как это необходимо, для реализации функциональности кроссплатформенного элемента управления на платформе. Кроме того, это может быть предоставлено дополнительным типом, который используется здесь.

Android

Видео воспроизводится в Android с VideoViewпомощью . Однако здесь VideoView он был инкапсулирован в MauiVideoPlayer тип, чтобы сохранить собственное представление отдельно от обработчика. В следующем примере показан частичный VideoHandler класс для Android с тремя переопределениями:

#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 производный от ViewHandler<TVirtualView,TPlatformView> класса, с универсальным Video аргументом, указывающим кроссплатформенный тип элемента управления, и MauiVideoPlayer аргумент, указывающий тип, инкапсулирующий VideoView собственное представление.

Переопределение CreatePlatformView создает и возвращает MauiVideoPlayer объект. Переопределение ConnectHandler — это расположение для выполнения любой требуемой настройки собственного представления. DisconnectHandler Переопределение — это расположение для выполнения любой очистки собственного представления, поэтому вызывает Dispose метод в экземпляреMauiVideoPlayer.

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления свойств:

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

Каждое действие выполняется в ответ на изменение свойства кроссплатформенного элемента управления и является static методом, который требует обработчика и межплатформенных экземпляров управления в качестве аргументов. В каждом случае действие вызывает метод, определенный в типе MauiVideoPlayer .

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления команд:

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

Каждое действие выполняется в ответ на команду, отправляемую из кроссплатформенного элемента управления, и является методом static , который требует обработчика и межплатформенных экземпляров управления, а также необязательные данные в качестве аргументов. В каждом случае действие вызывает метод, определенный в MauiVideoPlayer классе, после извлечения необязательных данных.

В Android MauiVideoPlayer класс инкапсулирует VideoView собственный вид, разделенный от обработчика:

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 производный от CoordinatorLayout, так как корневое собственное представление в приложении .NET MAUI в Android CoordinatorLayout. MauiVideoPlayer Хотя класс может быть производным от других собственных типов Android, в некоторых сценариях может быть трудно управлять размещением собственного представления.

Его VideoView можно добавить непосредственно CoordinatorLayoutв макет и разместить в макете по мере необходимости. Однако здесь в android RelativeLayout добавляется CoordinatorLayoutи VideoView добавляется в нее RelativeLayout. Параметры макета задаются как на RelativeLayout том, так и VideoView таким образом, что VideoView центрируется на странице, и расширяется, чтобы заполнить доступное пространство при сохранении его пропорции.

Конструктор также подписывается на VideoView.Prepared событие. Это событие возникает при готовности видео к воспроизведению и отменяет подписку в Dispose переопределении:

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

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

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

Помимо отмены подписки из события, Dispose переопределение также выполняет очистку собственного Prepared представления.

Примечание.

Dispose Переопределение вызывается переопределением обработчикаDisconnectHandler.

Элементы управления транспортировкой платформы включают кнопки воспроизведения, приостановки и остановки видео, а также предоставляются типом Android MediaController . Video.AreTransportControlsEnabled Если для свойства задано trueзначение, MediaController то в качестве проигрывателя VideoViewмультимедиа задано значение . Это происходит из-за того, что при AreTransportControlsEnabled установке свойства приложение сопоставления свойств обработчика гарантирует, что MapAreTransportControlsEnabled метод вызывается, что, в свою очередь, вызывает UpdateTransportControlsEnabled метод в 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;
            }
        }
    }
    ...
}

Элементы управления транспортом исчезают, если они не используются, но их можно восстановить, нажав на видео.

Video.AreTransportControlsEnabled Если для свойства задано falseзначение, MediaController он удаляется в качестве проигрывателя мультимедиа объектаVideoView. В этом сценарии вы можете программным образом управлять воспроизведением видео или предоставлять собственные элементы управления транспортировкой. Дополнительные сведения см. в разделе "Создание пользовательских элементов управления транспортировкой".

IOS и Mac Catalyst

Видео воспроизводится в iOS и Mac Catalyst с AVPlayer помощью iOS и an AVPlayerViewController. Однако здесь эти типы инкапсулируются в типе MauiVideoPlayer , чтобы сохранить собственные представления отдельно от обработчика. В следующем примере показан частичный VideoHandler класс для iOS с тремя переопределениями:

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 производный от ViewHandler<TVirtualView,TPlatformView> класса, с универсальным Video аргументом, указывающим кроссплатформенный тип элемента управления, и MauiVideoPlayer аргумент, указывающий тип, инкапсулирующий AVPlayer и AVPlayerViewController собственные представления.

Переопределение CreatePlatformView создает и возвращает MauiVideoPlayer объект. Переопределение ConnectHandler — это расположение для выполнения любой требуемой настройки собственного представления. DisconnectHandler Переопределение — это расположение для выполнения любой очистки собственного представления, поэтому вызывает Dispose метод в экземпляреMauiVideoPlayer.

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления свойств:

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

Каждое действие выполняется в ответ на изменение свойства кроссплатформенного элемента управления и является static методом, который требует обработчика и межплатформенных экземпляров управления в качестве аргументов. В каждом случае действие вызывает метод, определенный в типе MauiVideoPlayer .

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления команд:

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

Каждое действие выполняется в ответ на команду, отправляемую из кроссплатформенного элемента управления, и является методом static , который требует обработчика и межплатформенных экземпляров управления, а также необязательные данные в качестве аргументов. В каждом случае действие вызывает метод, определенный в MauiVideoPlayer классе, после извлечения необязательных данных.

В iOS и Mac Catalyst класс инкапсулирует инкапсулирует MauiVideoPlayer AVPlayer собственные AVPlayerViewController представления, разделенные от обработчика:

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 является производным от UIViewбазового класса в iOS и Mac Catalyst для объектов, которые отображают содержимое и обрабатывают взаимодействие пользователя с этим содержимым. Конструктор создает AVPlayer объект, который управляет воспроизведением и временем воспроизведения файла мультимедиа и задает его в качестве Player значения AVPlayerViewControllerсвойства объекта. Отображает содержимое AVPlayerViewController из и AVPlayer представляет элементы управления транспортом и другие функции. Затем устанавливается размер и расположение элемента управления, что гарантирует, что видео находится в центре страницы и расширяется, чтобы заполнить доступное пространство при сохранении его пропорции. В iOS 16 и Mac Catalyst 16 AVPlayerViewController необходимо добавить в родительский элемент ViewController для приложений на основе оболочки, в противном случае элементы управления транспортом не отображаются. Затем на страницу добавляется собственное представление, которое является представлением из AVPlayerViewControllerнее.

Метод Dispose отвечает за очистку собственного представления:

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

В некоторых сценариях видео продолжают воспроизводиться после перехода страницы воспроизведения видео. Чтобы остановить видео, ReplaceCurrentItemWithPlayerItem устанавливается null значение в Dispose переопределении и выполняется очистка другого собственного представления.

Примечание.

Dispose Переопределение вызывается переопределением обработчикаDisconnectHandler.

Элементы управления транспортировкой платформы включают кнопки воспроизведения, приостановки и остановки видео, а также предоставляются типом AVPlayerViewController . Video.AreTransportControlsEnabled Если для свойства задано значениеtrue, будет AVPlayerViewController отображаться его элементы управления воспроизведением. Это происходит из-за того, что при AreTransportControlsEnabled установке свойства приложение сопоставления свойств обработчика гарантирует, что MapAreTransportControlsEnabled метод вызывается, что, в свою очередь, вызывает UpdateTransportControlsEnabled метод в MauiVideoPlayer:

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

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

Элементы управления транспортом исчезают, если они не используются, но их можно восстановить, нажав на видео.

Video.AreTransportControlsEnabled Если для свойства задано falseзначение, он AVPlayerViewController не отображает элементы управления воспроизведением. В этом сценарии вы можете программным образом управлять воспроизведением видео или предоставлять собственные элементы управления транспортировкой. Дополнительные сведения см. в разделе "Создание пользовательских элементов управления транспортировкой".

Windows

Видео воспроизводится в Windows с MediaPlayerElementпомощью . Однако здесь MediaPlayerElement он был инкапсулирован в MauiVideoPlayer тип, чтобы сохранить собственное представление отдельно от обработчика. В следующем примере показан частичный VideoHandler класс fo Windows с тремя переопределениями:

#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 производный от ViewHandler<TVirtualView,TPlatformView> класса, с универсальным Video аргументом, указывающим кроссплатформенный тип элемента управления, и MauiVideoPlayer аргумент, указывающий тип, инкапсулирующий MediaPlayerElement собственное представление.

Переопределение CreatePlatformView создает и возвращает MauiVideoPlayer объект. Переопределение ConnectHandler — это расположение для выполнения любой требуемой настройки собственного представления. DisconnectHandler Переопределение — это расположение для выполнения любой очистки собственного представления, поэтому вызывает Dispose метод в экземпляреMauiVideoPlayer.

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления свойств:

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

Каждое действие выполняется в ответ на изменение свойства кроссплатформенного элемента управления и является static методом, который требует обработчика и межплатформенных экземпляров управления в качестве аргументов. В каждом случае действие вызывает метод, определенный в типе MauiVideoPlayer .

Обработчик платформы также должен реализовать действия, определенные в словаре сопоставления команд:

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

Каждое действие выполняется в ответ на команду, отправляемую из кроссплатформенного элемента управления, и является методом static , который требует обработчика и межплатформенных экземпляров управления, а также необязательные данные в качестве аргументов. В каждом случае действие вызывает метод, определенный в MauiVideoPlayer классе, после извлечения необязательных данных.

В MauiVideoPlayer Windows класс инкапсулирует MediaPlayerElement собственный вид, разделенный от обработчика:

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 производный от Grid, и MediaPlayerElement добавляется в качестве дочернего Gridэлемента . Это позволяет MediaPlayerElement автоматически заполнять все доступное пространство.

Метод Dispose отвечает за очистку собственного представления:

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

Помимо отмены подписки из события, Dispose переопределение также выполняет очистку собственного MediaOpened представления.

Примечание.

Dispose Переопределение вызывается переопределением обработчикаDisconnectHandler.

Элементы управления транспортировкой платформы включают кнопки воспроизведения, приостановки и остановки видео, а также предоставляются типом MediaPlayerElement . Video.AreTransportControlsEnabled Если для свойства задано значениеtrue, будет MediaPlayerElement отображаться его элементы управления воспроизведением. Это происходит из-за того, что при AreTransportControlsEnabled установке свойства приложение сопоставления свойств обработчика гарантирует, что MapAreTransportControlsEnabled метод вызывается, что, в свою очередь, вызывает UpdateTransportControlsEnabled метод в MauiVideoPlayer:

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

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

}

Video.AreTransportControlsEnabled Если для свойства задано falseзначение, он MediaPlayerElement не отображает элементы управления воспроизведением. В этом сценарии вы можете программным образом управлять воспроизведением видео или предоставлять собственные элементы управления транспортировкой. Дополнительные сведения см. в разделе "Создание пользовательских элементов управления транспортировкой".

Преобразование кроссплатформенного элемента управления в элемент управления платформы

Любой кроссплатформенный элемент управления .NET MAUI, производный от Element, можно преобразовать в базовый элемент управления платформы с ToPlatform помощью метода расширения:

  • В Android ToPlatform преобразует элемент управления MAUI .NET в объект Android View .
  • В iOS и Mac Catalyst ToPlatform преобразует элемент управления MAUI .NET в UIView объект.
  • В Windows ToPlatform преобразует элемент управления MAUI .NET в FrameworkElement объект.

Примечание.

Метод ToPlatform находится в Microsoft.Maui.Platform пространстве имен.

Для всех платформ ToPlatform метод требует аргумента MauiContext .

Метод ToPlatform может преобразовать кроссплатформенный элемент управления в базовый элемент управления платформы из кода платформы, например в частичном классе обработчика для платформы:

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

В этом примере в частичном VideoHandler классе для Android MapSource метод преобразует Video экземпляр в MauiVideoPlayer объект.

Метод ToPlatform также может преобразовать кроссплатформенный элемент управления в базовый элемент управления платформы из кроссплатформенного кода:

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

В этом примере кроссплатформенный Video video элемент управления преобразуется в базовое собственное представление на каждой OnHandlerChanged() платформе в переопределении. Это переопределение вызывается, когда собственное представление, реализующее кроссплатформенный элемент управления, доступно и инициализировано. Объект, возвращаемый методом ToPlatform , может быть приведение к точному собственному типу, который здесь является MauiVideoPlayer.

Воспроизведение видео

Класс Video определяет Source свойство, которое используется для указания источника видеофайла и AutoPlay свойства. AutoPlay значение trueпо умолчанию, что означает, что видео должно автоматически воспроизводиться после Source установки. Определение этих свойств см. в разделе "Создание кроссплатформенного элемента управления".

Свойство Source имеет тип VideoSource, который является абстрактным классом, состоящим из трех статических методов, создающих три класса, производных от 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 };
        }
    }
}

Класс VideoSource содержит атрибут TypeConverter, который ссылается на класс 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.");
        }
    }
}

Преобразователь типов вызывается, если Source для свойства задана строка в XAML. Метод ConvertFromInvariantString пытается преобразовать строку в объект Uri. Если она выполнена успешно, и схема не fileвыполнена, метод возвращает значение UriVideoSource. В противном случае возвращается ResourceVideoSourceзначение .

Воспроизведение веб-видео

Класс UriVideoSource используется для указания удаленного видео с универсальным кодом ресурса (URI). Он определяет Uri свойство типа 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); }
        }
    }
}

Source Если для свойства задано UriVideoSourceзначение, функция сопоставления свойств обработчика гарантирует, что MapSource метод вызывается:

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

Метод MapSource в очереди вызывает UpdateSource метод в свойстве обработчика PlatformView . Свойство PlatformView , которое имеет тип MauiVideoPlayer, представляет собственное представление, которое предоставляет реализацию видеопроигрывтеля на каждой платформе.

Android

Видео воспроизводится в Android с VideoViewпомощью . В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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();
            }
        }
        ...
    }
}

При обработке объектов типа UriVideoSourceSetVideoUri метод VideoView используется для указания воспроизведения видео с объектом AndroidUri, созданным из строкового URI.

Свойство AutoPlay не имеет эквивалента VideoView, поэтому Start метод вызывается, если задано новое видео.

IOS и Mac Catalyst

Для воспроизведения видео на iOS и Mac Catalyst создается объект типа AVAsset для инкапсулации видео, который используется для создания AVPlayerItemобъекта, который затем передается объекту AVPlayer . В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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();
            }
        }
        ...
    }
}

При обработке объектов типа UriVideoSourceстатический AVAsset.FromUrl метод используется для указания воспроизведения видео с объектом iOS NSUrl , созданным из строкового URI.

Свойство AutoPlay не имеет эквивалента в классах видео iOS, поэтому свойство проверяется в конце UpdateSource метода для вызова Play метода в объекте AVPlayer .

В некоторых случаях в iOS видео продолжают воспроизводиться после перехода на страницу воспроизведения видео. Чтобы остановить видео, ReplaceCurrentItemWithPlayerItem установите null значение в Dispose переопределении:

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

Windows

Видео воспроизводится в Windows с помощью MediaPlayerElement. В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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;
            }
        }
        ...
    }
}

При обработке объектов типа UriVideoSourceMediaPlayerElement.Source свойство присваивается MediaSource объекту, который инициализирует Uri URI видео, который будет воспроизводиться. MediaPlayerElement.Source При установке OnMediaPlayerMediaOpened метод обработчика событий регистрируется в событииMediaPlayerElement.MediaPlayer.MediaOpened. Этот обработчик событий используется для задания Duration свойства Video элемента управления.

В конце UpdateSource метода свойство проверяется и если свойство имеет значение trueMediaPlayerElement.AutoPlay, Video.AutoPlay то для начала воспроизведения видео задано true значение true.

Воспроизведение видеоресурса

Класс ResourceVideoSource используется для доступа к видеофайлам, внедренным в приложение. Он определяет Path свойство типа 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); }
        }
    }
}

Source Если для свойства задано ResourceVideoSourceзначение, функция сопоставления свойств обработчика гарантирует, что MapSource метод вызывается:

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

Метод MapSource в очереди вызывает UpdateSource метод в свойстве обработчика PlatformView . Свойство PlatformView , которое имеет тип MauiVideoPlayer, представляет собственное представление, которое предоставляет реализацию видеопроигрывтеля на каждой платформе.

Android

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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;
                }
            }
            ...
        }
        ...
    }
}

При обработке объектов типа ResourceVideoSourceSetVideoPath метод VideoView используется для указания воспроизведения видео с строковым аргументом, объединяющим имя пакета приложения с именем файла видео.

Видеофайл ресурса хранится в папке ресурсов пакета и требует от поставщика содержимого для доступа к нему. Поставщик содержимого предоставляется классом VideoProvider , который создает AssetFileDescriptor объект, предоставляющий доступ к видеофайлу:

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 и Mac Catalyst

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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);
                }
            }
            ...
        }
        ...
    }
}

При обработке объектов типа ResourceVideoSourceGetUrlForResource метод NSBundle используется для извлечения файла из пакета приложения. Полный путь необходимо разделить на имя файла, расширение и каталог.

В некоторых случаях в iOS видео продолжают воспроизводиться после перехода на страницу воспроизведения видео. Чтобы остановить видео, ReplaceCurrentItemWithPlayerItem установите null значение в Dispose переопределении:

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

Windows

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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;
                }
            }
            ...
        }
        ...
    }
}

При обработке объектов типа ResourceVideoSourceMediaPlayerElement.Source свойство присваивается MediaSource объекту, который инициализирует Uri путь к ресурсу видео с префиксомms-appx:///.

Воспроизведение видеофайла из библиотеки устройства

Класс FileVideoSource используется для доступа к видео в видеотеке устройства. Он определяет File свойство типа 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); }
        }
    }
}

Source Если для свойства задано FileVideoSourceзначение, функция сопоставления свойств обработчика гарантирует, что MapSource метод вызывается:

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

Метод MapSource в очереди вызывает UpdateSource метод в свойстве обработчика PlatformView . Свойство PlatformView , которое имеет тип MauiVideoPlayer, представляет собственное представление, которое предоставляет реализацию видеопроигрывтеля на каждой платформе.

Android

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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;
                }
            }
            ...
        }
        ...
    }
}

При обработке объектов типа FileVideoSourceSetVideoPath метод VideoView используется для указания воспроизведения видеофайла.

IOS и Mac Catalyst

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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 }));
            }
            ...
        }
        ...
    }
}

При обработке объектов типа FileVideoSourceстатический AVAsset.FromUrl метод используется для указания воспроизводимого видеофайла с NSUrl.CreateFileUrl помощью метода создания объекта iOS NSUrl из строкового URI.

Windows

В следующем примере кода показано, как UpdateSource метод обрабатывает Source свойство при его типе 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;
                }
            }
            ...
        }
        ...
    }
}

При обработке объектов типа FileVideoSourceимя видеофайла преобразуется в StorageFile объект. MediaSource.CreateFromStorageFile Затем метод возвращает объект, заданный MediaSource в качестве значения MediaPlayerElement.Source свойства.

Цикл видео

Класс Video определяет IsLooping свойство, которое позволяет элементу управления автоматически задать положение видео в начале после достижения его конца. Значение по умолчанию falseуказывает, что видео не циклит автоматически.

IsLooping Если свойство задано, функция сопоставления свойств обработчика гарантирует, что MapIsLooping метод вызывается:

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

Метод MapIsLooping в свою очередь вызывает UpdateIsLooping метод в свойстве обработчика PlatformView . Свойство PlatformView , которое имеет тип MauiVideoPlayer, представляет собственное представление, которое предоставляет реализацию видеопроигрывтеля на каждой платформе.

Android

В следующем примере кода показано, как UpdateIsLooping метод в Android включает цикл видео:

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

Чтобы включить цикл видео, MauiVideoPlayer класс реализует MediaPlayer.IOnPreparedListener интерфейс. Этот интерфейс определяет обратный OnPrepared вызов, который вызывается, когда источник мультимедиа готов к воспроизведению. Video.IsLooping Если свойство имеет значениеtrue, UpdateIsLooping метод задает MauiVideoPlayer в качестве объекта, который предоставляет обратный OnPrepared вызов. Обратный вызов задает MediaPlayer.IsLooping свойству значение Video.IsLooping свойства.

IOS и Mac Catalyst

В следующем примере кода показано, как UpdateIsLooping метод в iOS и Mac Catalyst включает цикл видео:

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

В iOS и Mac Catalyst уведомление используется для выполнения обратного вызова при воспроизведении видео до конца. Video.IsLooping Если свойство равноtrue, UpdateIsLooping метод добавляет наблюдателя для AVPlayerItem.DidPlayToEndTimeNotification уведомления и выполняет PlayedToEnd метод при получении уведомления. В свою очередь, этот метод возобновляет воспроизведение с начала видео. Video.IsLooping Если свойство равноfalse, видео приостанавливается в конце воспроизведения.

Так как MauiVideoPlayer добавляет наблюдателя для уведомления, он также должен удалить наблюдателя при выполнении очистки собственного представления. Это достигается в Dispose переопределении:

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

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

        base.Dispose(disposing);
    }

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

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

Переопределение Dispose вызывает DestroyPlayedToEndObserver метод, который удаляет наблюдателя для AVPlayerItem.DidPlayToEndTimeNotification уведомления, а также вызывает Dispose метод в объекте NSObject.

Windows

В следующем примере кода показано, как UpdateIsLooping метод в Windows включает цикл видео:

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

Чтобы включить цикл видео, UpdateIsLooping метод задает MediaPlayerElement.MediaPlayer.IsLoopingEnabled свойству значение Video.IsLooping свойства.

Создание пользовательских элементов управления транспортировкой

Элементы управления транспортом видеопроигрыватора включают кнопки, которые играют, приостанавливают и останавливают видео. Эти кнопки часто идентифицируются с знакомыми значками, а не текстом, а кнопки воспроизведения и приостановки часто объединяются в одну кнопку.

По умолчанию элемент Video управления отображает элементы управления транспортом, поддерживаемые каждой платформой. Однако при установке AreTransportControlsEnabled свойства falseэти элементы управления подавляются. Затем вы можете управлять воспроизведением видео программным способом или предоставлять собственные элементы управления транспортировкой.

Для реализации собственных элементов управления транспортом класс должен Video иметь возможность уведомлять свои собственные представления о воспроизведении, приостановке или остановке видео, а также знать текущее состояние воспроизведения видео. Класс Video определяет методы с именем Play, Pauseи Stop вызывает соответствующее событие и отправляет команду в 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);
        }
    }
}

Класс VideoPositionEventArgs определяет Position свойство, которое можно задать с помощью конструктора. Это свойство представляет позицию, с которой было запущено воспроизведение видео, приостановлено или остановлено.

Последняя строка в Playи PauseStop методах отправляет команду и связанные данныеVideoHandler. VideoHandler Имена CommandMapper команд сопоставляются с действиями, выполняемыми при получении команды. Например, при VideoHandler получении PlayRequested команды он выполняет свой MapPlayRequested метод. Преимущество этого подхода заключается в том, что он удаляет необходимость в собственных представлениях для подписки на события кроссплатформенного элемента управления и отмены подписки. Кроме того, это позволяет легко настраивать, так как средство сопоставления команд может быть изменено потребителями кроссплатформенного элемента управления без подклассов. Дополнительные сведения см. в CommandMapperстатье "Создание схемы команд".

Реализация MauiVideoPlayer в Android, iOS и Mac Catalyst имеет PauseRequestedPlayRequestedметоды, StopRequested выполняемые в ответ на отправку Video PlayRequestedэлементов управления и PauseRequestedStopRequested команды. Каждый метод вызывает метод в собственном представлении для воспроизведения, приостановки или остановки видео. Например, в следующем коде показаны PlayRequestedPauseRequestedStopRequested методы и методы в iOS и 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}.");
        }
    }
}

Каждый из трех методов регистрирует позицию, в которой видео было воспроизведено, приостановлено или остановлено, используя данные, отправленные с помощью команды.

Этот механизм гарантирует, что при PlayPauseвызове метода или Stop метода в Video элементе управления его собственное представление будет показано, как воспроизвести, приостановить или остановить видео, а также записать положение, в котором видео было воспроизведено, приостановлено или остановлено. Все это происходит с помощью развязанного подхода, без необходимости подписываться на кроссплатформенные события.

Состояние видео

Реализация функций воспроизведения, приостановки и остановки недостаточно для поддержки пользовательских элементов управления транспортировкой. Часто функция воспроизведения и приостановки должна быть реализована с той же кнопкой, которая изменяет его внешний вид, чтобы указать, воспроизводится ли видео или приостановлено. Кроме того, кнопка не должна быть включена, если видео еще не загружено.

Эти требования означают, что видеопроигрывателю необходимо сделать доступным текущее состояние, указывающее его актуальный режим — воспроизводит видео, приостановлен или пока не готов к воспроизведению видео. Это состояние может быть представлено перечислением:

public enum VideoStatus
{
    NotReady,
    Playing,
    Paused
}

Класс Video определяет привязываемое свойство только для чтения с именем Status типа VideoStatus. Это свойство определяется как доступное только для чтения, так как оно должно быть задано только из обработчика элемента управления:

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

Как правило, чтобы доступное только для чтения привязываемое свойство можно было задать в классе, оно должно иметь закрытый метод доступа set для свойства Status. Однако для View производных, поддерживаемых обработчиками, свойство должно быть задано вне класса, но только обработчиком элемента управления.

Поэтому другое свойство определяется с именем IVideoController.Status. Это явная реализация интерфейса, которая стала возможной благодаря интерфейсу IVideoController, реализуемому классом Video.

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

Этот интерфейс позволяет внешнему классу Video задать Status свойство, ссылаясь на IVideoController интерфейс. Свойство может быть задано из других классов и обработчика, но вряд ли будет задано непреднамеренно. Самое главное, Status свойство нельзя задать с помощью привязки данных.

Чтобы помочь реализации обработчика при Status обновлении свойства, Video класс определяет UpdateStatus событие и команду:

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

Обработчик OnTimerTick событий выполняется каждые десятые секунды, что вызывает UpdateStatus событие и вызывает UpdateStatus команду.

UpdateStatus Когда команда отправляется из Video элемента управления в обработчик, средство сопоставления команд обработчика гарантирует, что MapUpdateStatus метод вызывается:

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

Метод MapUpdateStatus в очереди вызывает UpdateStatus метод в свойстве обработчика PlatformView . Свойство PlatformView типа инкапсулирует собственные представления, MauiVideoPlayerобеспечивающие реализацию видеопроигрывтеля на каждой платформе.

Android

В следующем примере кода показан UpdateStatus метод в Android, который задает Status свойство:

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

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

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

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

            base.Dispose(disposing);
        }

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

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

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

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

Это VideoView.IsPlaying логическое свойство, указывающее, воспроизводит ли видео или приостановлено. Чтобы определить, не удается ли VideoView воспроизвести или приостановить видео, его Prepared событие должно быть обработано. Это событие возникает, когда источник мультимедиа готов к воспроизведению. Событие подписывается в MauiVideoPlayer конструкторе и отменяет подписку в его Dispose переопределении. Затем UpdateStatus метод использует isPrepared поле и VideoView.IsPlaying свойство, чтобы задать Status свойство объекта Video , присвоив ему значение IVideoController.

IOS и Mac Catalyst

В следующем примере кода показан UpdateStatus метод в iOS и Mac Catalyst, который задает Status свойство:

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

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

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

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

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

Для задания Status свойства необходимо получить доступ к двум свойствам AVPlayerStatus свойству типа AVPlayerStatus и TimeControlStatus свойству типаAVPlayerTimeControlStatus. Затем Status свойство можно задать для Video объекта, присвоив ему IVideoControllerзначение .

Windows

В следующем примере кода показан UpdateStatus метод в Windows, который задает Status свойство:

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

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

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

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

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

Метод UpdateStatus использует значение MediaPlayerElement.MediaPlayer.CurrentState свойства для определения значения Status свойства. Затем Status свойство можно задать для Video объекта, присвоив ему IVideoControllerзначение .

Позиционирование панели

Элементы управления транспортом, реализованные каждой платформой, включают позиционный бар. Эта панель напоминает ползунок или полосу прокрутки и показывает текущее расположение видео в течение его общей длительности. Пользователи могут управлять положением на панели для перемещения вперед или назад к новой позиции в видео.

Для реализации собственной панели размещения требуется Video , чтобы класс знал длительность видео и его текущее положение в течение этой длительности.

Duration

Один из элементов информации, необходимый Video элементу управления для поддержки настраиваемой панели позиционирования, — это длительность видео. Класс Video определяет привязываемое свойство только для чтения с именем Durationтипа TimeSpan. Это свойство определяется как доступное только для чтения, так как оно должно быть задано только из обработчика элемента управления:

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

Как правило, чтобы доступное только для чтения привязываемое свойство можно было задать в классе, оно должно иметь закрытый метод доступа set для свойства Duration. Однако для View производных, поддерживаемых обработчиками, свойство должно быть задано вне класса, но только обработчиком элемента управления.

Примечание.

Обработчик событий, измененных свойством для Duration привязываемого свойства, вызывает метод с именем SetTimeToEnd, который описывается в разделе "Вычисление времени окончания".

Поэтому другое свойство определяется с именем IVideoController.Duration. Это явная реализация интерфейса, которая стала возможной благодаря интерфейсу IVideoController, реализуемому классом Video.

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

Этот интерфейс позволяет внешнему классу Video задать Duration свойство, ссылаясь на IVideoController интерфейс. Свойство может быть задано из других классов и обработчика, но вряд ли будет задано непреднамеренно. Самое главное, Duration свойство нельзя задать с помощью привязки данных.

Длительность видео недоступна сразу после Source задания свойства Video элемента управления. Видео должно быть частично загружено, прежде чем собственное представление может определить его длительность.

Android

В Android VideoView.Duration свойство сообщает допустимую длительность в миллисекундах после VideoView.Prepared возникновения события. Класс MauiVideoPlayer использует Prepared обработчик событий для получения Duration значения свойства:

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

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

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

В iOS и Mac Catalyst длительность видео получается из AVPlayerItem.Duration свойства, но не сразу после AVPlayerItem создания. Можно задать наблюдатель iOS для Duration свойства, но MauiVideoPlayer класс получает длительность в UpdateStatus методе, который вызывается 10 раз в секунду:

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

Метод ConvertTime преобразует объект CMTime в значение TimeSpan.

Windows

В Windows MediaPlayerElement.MediaPlayer.NaturalDuration свойство является значением TimeSpan , которое становится допустимым при MediaPlayerElement.MediaPlayer.MediaOpened возникновении события. Класс MauiVideoPlayer использует MediaOpened обработчик событий для получения NaturalDuration значения свойства:

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

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

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

Затем OnMediaPlayer обработчик событий вызывает MainThread.BeginInvokeOnMainThread метод, чтобы задать Duration свойство объекта Video , присвоив ему IVideoControllerзначение в основном потоке. Это необходимо, так как MediaPlayerElement.MediaPlayer.MediaOpened событие обрабатывается в фоновом потоке. Дополнительные сведения о выполнении кода в основном потоке см. в разделе "Создание потока в потоке пользовательского интерфейса .NET MAUI".

Position

Элемент Video управления также требует Position свойства, увеличивающееся от нуля до Duration воспроизведения видео. Класс Video реализует это свойство как привязываемое свойство с общедоступными get и set методами доступа:

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

Метод get доступа возвращает текущее положение видео в качестве воспроизведения. Метод set доступа реагирует на манипуляцию пользователем с позицией панели, перемещая положение видео вперед или назад.

Примечание.

Обработчик событий, измененных свойством для Position привязываемого свойства, вызывает метод с именем SetTimeToEnd, который описывается в разделе "Вычисление времени окончания".

В Android, iOS и Mac Catalyst свойство, которое получает текущую get позицию, имеет только метод доступа. Вместо этого Seek метод доступен для задания позиции. Это, кажется, более разумный подход, чем использование одного Position свойства, которое имеет присущую проблему. Как видео воспроизводится, Position свойство должно постоянно обновляться, чтобы отразить новую позицию. Но вы не хотите, чтобы большинство изменений Position свойства заставило видеопроигрыватель перейти на новую позицию в видео. В таких случаях видеопроигрыватель осуществлял бы поиск по последнему значению свойства Position, в результате чего воспроизведение видео не продвигалось бы.

Несмотря на трудности реализации Position свойства с get помощью и set методы доступа, этот подход используется, так как он может использовать привязку данных. Свойство Position Video элемента управления может быть привязано к Slider объекту, который используется как для отображения позиции, так и для поиска новой позиции. Однако при реализации Position свойства необходимо несколько мер предосторожности, чтобы избежать циклов обратной связи.

Android

В Android VideoView.CurrentPosition свойство указывает текущее положение видео. Класс MauiVideoPlayer задает свойство в UpdateStatus методе одновременно, когда он задает Position Duration свойство:

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

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

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

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

Каждый раз, когда Position свойство задается методомUpdateStatus, свойство запускает PropertyChanged событие, Position которое приводит к вызову метода с помощью схемы свойств обработчикаUpdatePosition. Метод UpdatePosition не должен ничего делать для большинства изменений свойств. В противном случае при каждом изменении положения видео оно будет перемещено в то же положение, что только что достигнуто. Чтобы избежать этого цикла обратной связи, единственный вызывает Seek метод объектаVideoView, UpdatePosition если разница между Position свойством и текущей позицией VideoView объекта превышает одну секунду.

IOS и Mac Catalyst

В iOS и Mac Catalyst AVPlayerItem.CurrentTime свойство указывает текущее положение видео. Класс MauiVideoPlayer задает свойство в UpdateStatus методе одновременно, когда он задает Position Duration свойство:

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

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

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

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

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

Каждый раз, когда Position свойство задается методомUpdateStatus, свойство запускает PropertyChanged событие, Position которое приводит к вызову метода с помощью схемы свойств обработчикаUpdatePosition. Метод UpdatePosition не должен ничего делать для большинства изменений свойств. В противном случае при каждом изменении положения видео оно будет перемещено в то же положение, что только что достигнуто. Чтобы избежать этого цикла обратной связи, единственный вызывает Seek метод объектаAVPlayer, UpdatePosition если разница между Position свойством и текущей позицией AVPlayer объекта превышает одну секунду.

Windows

В Windows MediaPlayerElement.MedaPlayer.Position свойство указывает текущее положение видео. Класс MauiVideoPlayer задает свойство в UpdateStatus методе одновременно, когда он задает Position Duration свойство:

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

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

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

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

Каждый раз, когда Position свойство задается методомUpdateStatus, свойство запускает PropertyChanged событие, Position которое приводит к вызову метода с помощью схемы свойств обработчикаUpdatePosition. Метод UpdatePosition не должен ничего делать для большинства изменений свойств. В противном случае при каждом изменении положения видео оно будет перемещено в то же положение, что только что достигнуто. Чтобы избежать этого цикла обратной связи, только задает свойство, UpdatePosition если разница между Position свойством и текущей позицией MediaPlayerElement больше одной секунды.MediaPlayerElement.MediaPlayer.Position

Вычисление времени окончания

В некоторых случаях видеопроигрыватель отображает оставшееся время воспроизведения видео. Это значение начинается с длительности видео, когда начинается видео, и уменьшается до нуля, когда видео заканчивается.

Класс Video включает свойство только для TimeToEnd чтения, вычисляемое на основе изменений Duration и 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;
        }
        ...
    }
}

Метод SetTimeToEnd вызывается из обработчиков Duration событий, измененных свойств, и Position свойств.

Настраиваемая панель позиционирования

Пользовательская панель позиционирования может быть реализована путем создания класса, наследуемого от Sliderкласса, содержащего Duration и Position свойства типа 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;
                }
            };
        }
    }
}

Обработчик событий, измененных свойством для Duration свойства, задает Maximum свойство Slider TotalSeconds свойства TimeSpan значения. Аналогичным образом обработчик событий, измененных свойством для Position свойства, задает Value свойство Sliderобъекта. Это механизм, с помощью которого Slider отслеживается положение PositionSlider.

Он PositionSlider обновляется только из базового Slider сценария, то есть когда пользователь управляет Slider этим, чтобы указать, что видео должно быть расширено или отменено на новую позицию. Это обнаружено в PropertyChanged обработчике конструктора PositionSlider . Этот обработчик событий проверяет изменение свойства Value и, если оно отличается от Position свойства, Position свойство задается из Value свойства.

Регистрация обработчика

Пользовательский элемент управления и его обработчик должны быть зарегистрированы в приложении, прежде чем его можно будет использовать. Это должно произойти в CreateMauiApp MauiProgram классе в проекте приложения, который является кроссплатформенной точкой входа для приложения:

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

Обработчик регистрируется в и AddHandler методеConfigureMauiHandlers. Первым аргументом метода AddHandler является кроссплатформенный тип элемента управления, а второй аргумент является его типом обработчика.

Использование кроссплатформенного элемента управления

После регистрации обработчика в приложении можно использовать кроссплатформенный элемент управления.

Воспроизведение веб-видео

Элемент Video управления может воспроизводить видео из URL-адреса, как показано в следующем примере:

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

В этом примере VideoSourceConverter класс преобразует строку, представляющую универсальный код ресурса (URI) в a UriVideoSource. Затем видео начинает загрузку и начинает воспроизводиться после загрузки и буферизации достаточного количества данных. На каждой платформе элементы управления транспортом исчезают, если они не используются, но их можно восстановить, нажав на видео.

Воспроизведение видеоресурса

Видеофайлы, внедренные в папку Resources\Raw приложения, с действием сборки MauiAsset , можно воспроизвести элементом Video управления:

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

В этом примере VideoSourceConverter класс преобразует строку, представляющую имя файла видео в объект ResourceVideoSource. Для каждой платформы видео начинает воспроизводиться почти сразу после установки источника видео, так как файл находится в пакете приложения и не должен быть скачан. На каждой платформе элементы управления транспортом исчезают, если они не используются, но их можно восстановить, нажав на видео.

Воспроизведение видеофайла из библиотеки устройства

Видеофайлы, хранящиеся на устройстве, можно получить, а затем воспроизвести с Video помощью элемента управления:

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

Button Когда выполняется обработчик Clicked событий, показанный в следующем примере кода:

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

Обработчик Clicked событий использует класс MAUI MediaPicker .NET, чтобы пользователь выбрал видеофайл с устройства. Выбранный видеофайл затем инкапсулируется как FileVideoSource объект и устанавливается в качестве Source свойства Video элемента управления. Дополнительные сведения о классе см. в средстве MediaPickerвыбора мультимедиа. Для каждой платформы воспроизведение видео начинается почти сразу после задания источника видео, так как файл находится на устройстве и скачивать его не требуется. На каждой платформе элементы управления транспортом исчезают, если они не используются, но их можно восстановить, нажав на видео.

Настройка элемента управления "Видео"

Вы можете запретить автоматический запуск видео, задав AutoPlay для свойства falseзначение :

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

Вы можете отключить элементы управления транспортом AreTransportControlsEnabled , задав для свойства falseзначение :

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

Если вы устанавливаете AutoPlay и falseAreTransportControlsEnabled используете, видео не начнет воспроизводиться, и не будет никакого способа начать его воспроизведение. В этом сценарии необходимо вызвать Play метод из файла программной части или создать собственные элементы управления транспортировкой.

Кроме того, можно настроить цикл видео, задав IsLooping для свойства значение true:

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

Если для этого задано IsLooping свойство true , Video элемент управления автоматически устанавливает положение видео в начало после достижения его конца.

Использование пользовательских элементов управления транспортом

В следующем примере 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.CustomTransportPage"
             Unloaded="OnContentPageUnloaded"
             Title="Custom transport controls">
    <Grid x:DataType="controls:Video"
          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>

В этом примере Video элемент управления задает AreTransportControlsEnabled свойство false и определяет значение, которое воспроизводит и приостанавливает Button видео, а также Button останавливает воспроизведение видео. Внешний вид кнопки определяется с помощью символов юникода и их текстовых эквивалентов для создания кнопок, состоящих из значка и текста:

Снимок экрана: кнопки воспроизведения и приостановки.

При воспроизведении видео кнопка воспроизведения обновляется до кнопки приостановки:

Снимок экрана: кнопки приостановки и остановки.

Пользовательский интерфейс также включает ActivityIndicator отображаемое во время загрузки видео. Триггеры данных используются для включения и отключения ActivityIndicator кнопок, а также для переключения первой кнопки между воспроизведением и приостановкой. Дополнительные сведения об триггерах данных см. в разделе "Триггеры данных".

Файл программной части определяет обработчики событий для событий кнопки 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();
    }
    ...
}

Настраиваемая панель позиционирования

В следующем примере показана настраиваемая панель позиционирования, PositionSliderпотребляемая в 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 x:DataType="controls:Video"
          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>

Position Свойство Video объекта привязано к Position свойству PositionSliderобъекта без проблем с производительностью, так как Video.Position свойство изменяется методом MauiVideoPlayer.UpdateStatus на каждой платформе, который вызывается только 10 раз в секунду. Кроме того, два Label объекта отображают Position значения и TimeToEnd свойства из Video объекта.

Очистка собственного представления

Реализация обработчика каждой платформы переопределяет DisconnectHandler реализацию, которая используется для очистки собственного представления, например отмены подписки на события и удаления объектов. Однако это переопределение намеренно не вызывается .NET MAUI. Вместо этого необходимо вызвать его самостоятельно из подходящего расположения в жизненном цикле приложения. Это часто происходит при переходе страницы, Video содержащей элемент управления, что приводит к возникновению события страницы Unloaded .

Обработчик событий для события страницы Unloaded можно зарегистрировать в XAML:

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

Затем обработчик событий для Unloaded события может вызвать DisconnectHandler метод в своем Handler экземпляре:

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

Помимо очистки ресурсов собственного представления, вызов метода обработчика DisconnectHandler также гарантирует, что видео перестают воспроизводиться на обратной навигации в iOS.

Отключение обработчика управления

Реализация обработчика каждой платформы переопределяет DisconnectHandler реализацию, которая используется для очистки собственного представления, например отмены подписки на события и удаления объектов. По умолчанию обработчики автоматически отключают от своих элементов управления, например при переходе назад в приложении.

В некоторых сценариях может потребоваться контролировать, когда обработчик отключается от его элемента управления, что может быть достигнуто с присоединенным свойством HandlerProperties.DisconnectPolicy . Для этого свойства требуется HandlerDisconnectPolicy аргумент с перечислением, определяющим следующие значения:

  • Automatic, указывающее, что обработчик будет отключен автоматически. Это значение по умолчанию для присоединенного свойства HandlerProperties.DisconnectPolicy.
  • Manual, указывающее, что обработчику придется отключить вручную, вызвав реализацию DisconnectHandler() .

В следующем примере показано задание присоединенного HandlerProperties.DisconnectPolicy свойства:

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

Эквивалентный код на C# выглядит так:

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

При задании присоединенного HandlerProperties.DisconnectPolicy свойства Manual необходимо вызвать реализацию обработчика DisconnectHandler самостоятельно из подходящего расположения в жизненном цикле приложения. Это можно достичь путем video.Handler?.DisconnectHandler();вызова.

Кроме того, существует DisconnectHandlers метод расширения, который отключает обработчики от заданного:IView

video.DisconnectHandlers();

При отключении метод будет распространяться по дереву управления до тех пор, DisconnectHandlers пока он не завершится или не появится в элементе управления, настроив политику вручную.