次の方法で共有


ハンドラーを使用してカスタム コントロールを作成する

サンプルを参照します。 サンプルを参照する

アプリの標準的な要件は、ビデオを再生する機能です。 この記事では、.NET マルチプラットフォーム アプリ UI (.NET MAUI) クロスプラットフォーム Video コントロールを作成する方法について説明します。このコントロールは、ハンドラーを使用して、クロスプラットフォームコントロール APIを、動画を再生する Android、iOS、Mac Catalyst のネイティブビューにマッピングします。 このコントロールは、3 つのソースからビデオを再生できます。

  • リモート ビデオを表す URL。
  • リソース。アプリに埋め込まれたファイル。
  • デバイスのビデオ ライブラリからのファイル。

ビデオ コントロールには、「トランスポート コントロール」 (ビデオを再生および一時停止するためのボタン) と、ビデオの進行状況を表示したりユーザーが別の場所にすばやくスキップできたりする配置バーが必要です。 Video コントロールは、プラットフォームによって提供されるトランスポート コントロールと配置バーのどちらかを使えます。または、ご自身でカスタムのトランスポート コントロールと配置バーを提供できます。 次のスクリーンショットは、カスタム トランスポート コントロールがある場合とない場合の iOS 上のコントロールを示しています。

iOS でのビデオ再生のスクリーンショット。iOS のカスタム トランスポート コントロールを使用したビデオ再生のスクリーンショット。

より洗練されたビデオ プレーヤーには、音量調整や、電話がかかってきたときにビデオを中断するメカニズム、再生中に画面をアクティブ状態に保つ方法など、いくつかの追加機能が備わっています。

次の図に、 Video コントロールのアーキテクチャを示します:

ビデオ ハンドラーのアーキテクチャ。

Video クラスは、コントロールのクロスプラットフォーム API を提供します。 クロスプラットフォーム API からネイティブ ビュー API へのマッピングは、各プラットフォーム上の VideoHandler クラスによって実行され、 Video クラスがMauiVideoPlayer クラスにマップされます。 iOS および Mac Catalyst では、MauiVideoPlayer クラスは AVPlayer 型を使用してビデオ再生を提供します。 Android では、MauiVideoPlayer クラスは VideoView 型を使用してビデオ再生を提供します。 Windowsでは、 MauiVideoPlayer クラスは MediaPlayerElement 型を使用してビデオ再生を提供します。

重要

.NET MAUI は、インターフェイスを介して、そのハンドラーをクロスプラットフォーム コントロールから切り離します。 これにより、Comet や Fabulous などの実験的なフレームワークは、.NET MAUI のハンドラーを引き続き使用しながら、インターフェイスを実装する独自のクロスプラットフォーム コントロールを提供できます。 クロスプラットフォーム コントロールのインターフェイスの作成は、同様の目的やテスト目的で、ハンドラーをそのクロスプラットフォーム コントロールから切り離す必要がある場合にのみ必要です。

ハンドラーによってプラットフォーム実装が提供される、クロスプラットフォーム .NET MAUI カスタム コントロールを作成するプロセスは次のとおりです:

  1. コントロールのパブリック API を提供するクロスプラットフォーム コントロールのクラスを作成します。 詳細については、「クロスプラットフォームコントロールの作成」を参照してください。
  2. 必要なクロスプラットフォーム型を追加作成します。
  3. partial ハンドラー クラスを作成します。 詳細については、「ハンドラーを作成する」をご覧ください。
  4. ハンドラー クラスで、PropertyMapper ディクショナリを作成します。これは、クロスプラットフォームのプロパティ変更が発生したときに実行するアクションを定義します。 プロパティの詳細については、「プロパティマッパーの作成」を参照してください。
  5. 必要に応じて、ハンドラー クラスで CommandMapper ディクショナリを作成します。このディクショナリは、クロスプラットフォーム コントロールがクロスプラットフォーム コントロールを実装するネイティブ ビューに命令を送信するときに実行するアクションを定義します。 詳細については、「コマンドマッパーの作成」を参照してください。
  6. クロスプラットフォーム コントロールを実装するネイティブ ビューを作成する partial ハンドラー クラスを、プラットフォームごとに作成します。 詳細については、「プラットフォーム コントロールを作成する」をご覧ください。
  7. アプリの MauiProgram クラスの ConfigureMauiHandlers メソッドと AddHandler メソッドを使用してハンドラーを登録します。 詳細については、「ハンドラーを登録する」をご覧ください。

その後、クロスプラットフォーム コントロールを使用できます。 詳細については、「クロスプラットフォームコントロールの使用」を参照してください。

クロスプラットフォーム コントロールを作成する

クロスプラットフォーム コントロールを作成するには、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 ステートメントが、PlatformViewSystem.Objectと等しいことを定義しています。 これは、PlatformView 型をハンドラー内で使用してすべてのプラットフォームで使用できるようにするために必要です。 別の方法としては、条件付きコンパイルを使用して、プラットフォームごとに 1 回 PlatformView プロパティを定義する必要があります。

プロパティ マッパーを作成する

通常、各ハンドラーはプロパティ マッパーを提供します。これは、クロスプラットフォーム コントロールでプロパティの変更が発生したときに実行するアクションを定義します。 PropertyMapper 型は、クロスプラットフォーム コントロールのプロパティを関連するアクションにマッピングする Dictionary です。

PropertyMapper は .NET MAUI の ViewHandler<TVirtualView,TPlatformView> クラスで定義されており、2 つのジェネリック引数を指定する必要があります。

  • View から派生するクロスプラットフォーム コントロールのクラス。
  • ハンドラーのクラス。

次のコード例は、PropertyMapper 定義により拡張された VideoHandler クラスを示しています。

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

PropertyMapperDictionary で、そのキーは string で、その値はジェネリック Action です。 string はクロスプラットフォーム コントロールのプロパティ名を表し、Action は、引数としてハンドラーとクロスプラットフォーム コントロールを必要とする static メソッドを表します。 例えば、MapSource メソッドのシグネチャは public static void MapSource(VideoHandler handler, Video video) です。

各プラットフォーム ハンドラーは、ネイティブ ビュー API を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールでプロパティが設定されると、基になるネイティブ ビューが必要に応じて更新されます。 このアプローチの利点は、クロスプラットフォーム コントロール コンシューマーがサブクラス化せずにプロパティ マッパーを変更できるため、クロスプラットフォーム コントロールを簡単にカスタマイズできることです。

コマンド マッパーを作成する

各ハンドラーは、クロスプラットフォーム コントロールがネイティブ ビューにコマンドを送信するときに実行するアクションを定義するコマンド マッパーを提供することもできます。 コマンド マッパーはプロパティ マッパーに似ていますが、追加のデータを渡すことができます。 このコンテキストでは、コマンドは命令であり、必要に応じてそのデータがネイティブ ビューに送信されます。 CommandMapper 型は Dictionary で、クロスプラットフォーム コントロール メンバーを関連するアクションにマッピングします。

CommandMapper は .NET MAUI の ViewHandler<TVirtualView,TPlatformView> クラスで定義されており、2 つのジェネリック引数を指定する必要があります。

  • View から派生するクロスプラットフォーム コントロールのクラス。
  • ハンドラーのクラス。

次のコード例は、CommandMapper 定義により拡張された VideoHandler クラスを示しています。

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

CommandMapperDictionary で、キーは string、値はジェネリック Action です。 string は、クロスプラットフォーム コントロールのコマンド名を表し、Action は引数としてハンドラー、クロスプラットフォーム コントロール、および省略可能なデータを必要とする static メソッドを表します。 たとえば、MapPlayRequested メソッドのシグネチャは public static void MapPlayRequested(VideoHandler handler, Video video, object? args) です。

各プラットフォーム ハンドラーは、ネイティブ ビュー API を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールからコマンドが送信されると、基になるネイティブ ビューが必要に応じて操作されるようになります。 この方法の利点は、クロスプラットフォーム コントロール イベントのサブスクライブとサブスクライブ解除をネイティブ ビューで行う必要がなくなることです。 さらに、サブクラス化せずにクロスプラットフォーム コントロール コンシューマーによってコマンド マッパーを変更できるため、簡単にカスタマイズできます。

プラットフォーム コントロールの作成

ハンドラーのマッパーを作成した後、すべてのプラットフォームでハンドラーの実装を提供する必要があります。 これを実現するには、プラットフォーム フォルダーの子フォルダーに部分クラス ハンドラーの実装を追加します。 または、ファイル名ベースのマルチターゲット、フォルダーベースのマルチターゲット、またはその両方をサポートするようにプロジェクトを構成することもできます。

サンプル アプリは、ファイル名ベースのマルチターゲットをサポートするように構成されているため、ハンドラー クラスはすべて 1 つのフォルダーに配置されます。

プロジェクトの Handlers フォルダー内のファイルのスクリーンショット。

マッパーを含む VideoHandler クラスには、VideoHandler.cs という名前が付けられます。 そのプラットフォームの実装は、VideoHandler.Android.csVideoHandler.MaciOS.csVideoHandler.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> クラスから派生する必要があります。これには 2 つの型引数が必要です。

  • View から派生するクロスプラットフォーム コントロールのクラス。
  • プラットフォームでクロスプラットフォーム コントロールを実装するネイティブ ビューの型。 これは、ハンドラー内の PlatformView プロパティの型と同じである必要があります。

重要

ViewHandler<TVirtualView,TPlatformView> クラスは、VirtualView プロパティと PlatformView プロパティを提供します。 VirtualView プロパティは、ハンドラーからクロスプラットフォーム コントロールにアクセスするために使用されます。 PlatformView プロパティは、クロスプラットフォーム コントロールを実装する各プラットフォームのネイティブ ビューにアクセスするために使用されます。

プラットフォーム ハンドラーの実装はそれぞれ、次のメソッドをオーバーライドする必要があります。

  • CreatePlatformView は、クロスプラットフォーム コントロールを実装するネイティブ ビューを作成して返す必要があります。
  • ConnectHandler は、ネイティブ ビューの初期化やイベント サブスクリプションの実行など、ネイティブ ビューのセットアップを実行する必要があります。
  • DisconnectHandlerは、イベントからのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行する必要があります。

重要

DisconnectHandler メソッドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な場所から自分で呼び出す必要があります。 詳細については、「ネイティブ ビューのクリーンアップ」をご覧ください。

重要

DisconnectHandler メソッドは既定で .NET MAUI によって自動的に呼び出されますが、この動作は変更できます。 詳細については、「 Control ハンドラーの切断」を参照してください。

各プラットフォーム ハンドラーは、マッパー ディクショナリで定義されている各 Action も実装する必要があります。

さらに、各プラットフォーム ハンドラーは、プラットフォームにクロスプラットフォーム コントロールの機能を実装するために、必要に応じてコードを提供する必要もあります。 または、ここで採用されている手法である追加の型として提供することもできます。

Android

ビデオは、Android で VideoView を使用して再生されます。 ただし、ここでは、ネイティブ ビューをハンドラーから分離し続けるために、VideoViewMauiVideoPlayer 型にカプセル化されています。 次の例は、Android の VideoHandler 部分クラスとその 3 つのオーバーライドを示しています。

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

VideoHandlerViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は VideoView ネイティブ ビューをカプセル化する型を指定します。

CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。 ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。 DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、MauiVideoPlayer インスタンスで Dispose メソッドを呼び出します。

プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。

プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、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;
        }
        ...
    }
}

Android での.NET MAUI アプリのルート ネイティブ ビューは CoordinatorLayout であるため、MauiVideoPlayerCoordinatorLayout から派生します。 MauiVideoPlayer クラスは他のネイティブ Android 型から派生することもありますが、一部のシナリオではネイティブ ビューの配置の制御が難しくなる場合があります。

VideoViewCoordinatorLayout に直接追加することができ、必要に応じてレイアウト上での配置が可能です。 ただし、ここでは、Android RelativeLayoutCoordinatorLayout に追加され、VideoViewRelativeLayout に追加されます。 レイアウト パラメーターは RelativeLayoutVideoView の両方に設定され、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);
    }
    ...
}

Prepared イベントからの解除に加えて、Dispose オーバーライドは、ネイティブ ビューのクリーンアップも実行します。

Note

Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。

プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが実装されています。このボタンは Android の MediaController 型によって提供されます。 Video.AreTransportControlsEnabled プロパティが true に設定されている場合は、VideoView のメディア プレーヤーとして MediaController が設定されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に MauiVideoPlayerUpdateTransportControlsEnabled メソッドが呼び出されるために発生します。

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 に設定されている場合、MediaControllerVideoView のメディア プレーヤーとして削除されます。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。

iOS と Mac Catalyst

ビデオは、iOS と Mac Catalyst で AVPlayerAVPlayerViewController を使用して再生されます。 ただし、ここでは、これらの型は MauiVideoPlayer 型でカプセル化して、ネイティブ ビューをハンドラーから分離し続けます。 次の例は、iOS の VideoHandler 部分クラスとその 3 つのオーバーライドを示しています。

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

VideoHandlerViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は AVPlayerAVPlayerViewController ネイティブ ビューをカプセル化する型を指定します。

CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。 ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。 DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、MauiVideoPlayer インスタンスで Dispose メソッドを呼び出します。

プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。

プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、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);
        }
        ...
    }
}

UIView から派生する MauiVideoPlayer は、iOS および Mac Catalyst の基本クラスであり、コンテンツを表示し、そのコンテンツとのユーザー操作を処理するオブジェクトです。 このコンストラクターは、メディア ファイルの再生とタイミングを管理する AVPlayer オブジェクトを作成し、それを AVPlayerViewControllerPlayer プロパティ値として設定します。 AVPlayerViewControllerAVPlayer からのコンテンツを表示し、トランスポート コントロールとその他の機能を表示します。 コントロールのサイズと位置が設定されます。これにより、ビデオがページの中央に配置され、縦横比を維持しながら使用可能な領域を埋めるように拡張されます。 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);
    }
    ...
}

一部のシナリオでは、ビデオ再生ページから移動した後もビデオの再生が続行されます。 ビデオを停止するには、ReplaceCurrentItemWithPlayerItemDispose オーバーライドで null に設定し、その他のネイティブ ビュー クリーンアップを実行します。

Note

Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。

プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが含まれており、これらは AVPlayerViewController 型によって提供されます。 Video.AreTransportControlsEnabled プロパティが true に設定されている場合、AVPlayerViewController でその再生コントロールが表示されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に MauiVideoPlayerUpdateTransportControlsEnabled メソッドが呼び出されるために発生します。

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

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

トランスポート コントロールは使用されていない場合はフェード アウトしますが、ビデオをタップする復元します。

Video.AreTransportControlsEnabled プロパティが false に設定されている場合、AVPlayerViewController でその再生コントロールは表示されません。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。

Windows

ビデオは、Windows で MediaPlayerElement で再生されます。 ただし、ここでは、ネイティブ ビューをハンドラーから分離させたままにするため、MediaPlayerElementMauiVideoPlayer 型にカプセル化されています。 次の例は、Windows の VideoHandler 部分クラスとその 3 つのオーバーライドを示しています。

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

VideoHandlerViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は MediaPlayerElement ネイティブ ビューをカプセル化する型を指定します。

CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。 ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。 DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、MauiVideoPlayer インスタンスで Dispose メソッドを呼び出します。

プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。

プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。

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

各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、MauiVideoPlayer クラスで定義されたメソッドを呼び出します。

Windows では、ネイティブ ビューをハンドラーから分離させたままにするため、MauiVideoPlayer クラスは 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);
        }
        ...
    }
}

MauiVideoPlayerGrid から派生し、MediaPlayerElementGrid の子として追加されます。 これにより、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 イベントからのサブスクライブ解除に加えて、ネイティブ ビューのクリーンアップも実行します。

Note

Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。

プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが含まれており、これらは MediaPlayerElement 型によって提供されます。 Video.AreTransportControlsEnabled プロパティが true に設定されている場合、MediaPlayerElement でその再生コントロールが表示されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に MauiVideoPlayerUpdateTransportControlsEnabled メソッドが呼び出されるために発生します。

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

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

}

Video.AreTransportControlsEnabled プロパティが false に設定されている場合、MediaPlayerElement でその再生コントロールは表示されません。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。

クロスプラットフォーム コントロールをプラットフォーム コントロールに変換する

Element から派生した .NET MAUI クロスプラットフォーム コントロールは、ToPlatform 拡張メソッドを使用して、基になるプラットフォーム コントロールに変換できます。

  • Android では、ToPlatform は .NET MAUI コントロールを Android View オブジェクトに変換します。
  • iOS および Mac Catalyst では、ToPlatform は .NET MAUI コントロールを UIView オブジェクトに変換します。
  • Windows では、ToPlatform は .NET MAUI コントロールを FrameworkElement オブジェクトに変換します。

Note

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

この例では、Android の VideoHandler 部分クラスで、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 から派生する 3 つのクラスをインスタンス化する 3 つの静的メソッドで構成される抽象クラスです。

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 クラスには、VideoSourceConverter を参照する TypeConverter 属性が含まれています。

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

この型コンバーターは、XAML の文字列に Source プロパティが設定されると起動します。 ConvertFromInvariantString メソッドが文字列を Uri オブジェクトに変換しようとします。 これが成功し、スキームが file でない場合は、このメソッドは UriVideoSource を返します。 それ以外の場合は ResourceVideoSource を返します。

Web ビデオの再生

UriVideoSource クラスを使用すると、リモートのビデオを URI で指定できます。 これにより、string 型の Uri プロパティを定義します。

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 メソッドは、ハンドラーの PlatformView プロパティの UpdateSource メソッドを順番に呼び出します。 MauiVideoPlayer 型の PlatformView プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。

Android

ビデオは、Android で VideoView を使用して再生されます。 次のコード例は、UpdateSource メソッドが UriVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

UriVideoSource 型のオブジェクトを処理する場合は、VideoViewSetVideoUri メソッド を使用して再生するビデオを指定し、URI 文字列から Android の Uri オブジェクトを作成します。

AutoPlay プロパティには VideoView に相当するものがないため、新しいビデオが設定されると Start メソッドが呼び出されます。

iOS と Mac Catalyst

iOS と Mac Catalyst でビデオを再生するには、AVAsset 型のオブジェクトを作成してビデオがカプセル化され、それが AVPlayerItem の作成に使用されて AVPlayer オブジェクトに渡されます。 次のコード例は、UpdateSource メソッドが UriVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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 メソッドを使用して再生するビデオを指定し、URI 文字列から iOS NSUrl オブジェクトを作成します。

AutoPlay プロパティには iOS ビデオ クラスに相当するものがないため、このプロパティは UpdateSource メソッドの最後に検査され、AVPlayer オブジェクトの Play メソッドが呼び出されます。

iOS では、ビデオ再生ページから移動した後もビデオが再生され続ける場合があります。 ビデオを停止するには、Dispose オーバーライドで ReplaceCurrentItemWithPlayerItemnull に設定します。

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

Windows

Windows では、ビデオは MediaPlayerElement で再生されます。 次のコード例は、UpdateSource メソッドが UriVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

UriVideoSource 型のオブジェクトを処理する場合、MediaPlayerElement.Source プロパティには、再生するビデオの URI を使用して Uri を初期化する MediaSource オブジェクトが設定されます。 MediaPlayerElement.Source が設定されると、MediaPlayerElement.MediaPlayer.MediaOpened イベントに対して OnMediaPlayerMediaOpened イベント ハンドラー メソッドが登録されます。 このイベント ハンドラーは、Video コントロールの Duration プロパティを設定するために使用されます。

UpdateSource メソッドの最後に Video.AutoPlay プロパティが調べられ、true の場合は、ビデオ再生を開始するように MediaPlayerElement.AutoPlay プロパティが true に設定されます。

ビデオ リソースの再生

ResourceVideoSource クラスは、アプリに埋め込まれているビデオ ファイルにアクセスするために使用されます。 これにより、string 型の Path プロパティを定義します。

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 メソッドは、ハンドラーの PlatformView プロパティの UpdateSource メソッドを順番に呼び出します。 MauiVideoPlayer 型の PlatformView プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。

Android

次のコード例は、UpdateSource メソッドが ResourceVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

ResourceVideoSource 型のオブジェクトを処理する場合、VideoViewSetVideoPath メソッドを使用して再生するビデオを指定し、アプリのパッケージ名とビデオのファイル名を組み合わせた文字列引数を指定します。

リソース ビデオ ファイルはパッケージの アセット フォルダーに格納され、コンテンツ プロバイダーがアクセスする必要があります。 コンテンツ プロバイダーは 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 メソッドが ResourceVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

ResourceVideoSource 型のオブジェクトを処理する場合、NSBundleGetUrlForResource メソッドを使用してアプリ パッケージからファイルを取得します。 完全なパスをファイル名、拡張子、ディレクトリに分割する必要があります。

iOS では、ビデオ再生ページから移動した後もビデオが再生され続ける場合があります。 ビデオを停止するには、Dispose オーバーライドで ReplaceCurrentItemWithPlayerItemnull に設定します。

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

Windows

次のコード例は、UpdateSource メソッドが ResourceVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

ResourceVideoSource 型のオブジェクトを処理する場合、MediaPlayerElement.Source プロパティは、ms-appx:/// というプレフィックスが付いたビデオ リソースのパスで Uri を初期化する MediaSource オブジェクトに設定されます。

デバイスのライブラリからビデオ ファイルを再生する

FileVideoSource クラスは、デバイスのビデオ ライブラリからビデオ ファイルにアクセスするために使用します。 これにより、string 型の File プロパティを定義します。

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 メソッドは、ハンドラーの PlatformView プロパティの UpdateSource メソッドを順番に呼び出します。 MauiVideoPlayer 型の PlatformView プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。

Android

次のコード例は、UpdateSource メソッドが FileVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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

FileVideoSource型のオブジェクトを処理する場合は、VideoViewSetVideoPathメソッドで再生するビデオ ファイルを特定します。

iOS と Mac Catalyst

次のコード例は、UpdateSource メソッドが FileVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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 メソッドを使用して文字列 URI から iOS NSUrl オブジェクトが作成されます。

Windows

次のコード例は、UpdateSource メソッドが FileVideoSource 型の場合に Source プロパティを処理する方法を示しています。

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 メソッドは、MediaPlayerElement.Source プロパティの値として設定される MediaSource オブジェクトを返します。

ビデオのループ

Video クラスは IsLooping プロパティを定義します。これにより、コントロールはビデオの位置が末尾に到達すると、自動的に開始位置に設定します。 既定では false であり、ビデオが自動的にループしないことを示します。

IsLooping プロパティが設定されると、ハンドラーのプロパティ マッパーによって、MapIsLooping メソッドが確実に呼び出されます。

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

MapIsLooping メソッドは、ハンドラーの PlatformView プロパティの UpdateIsLooping メソッドを呼び出します。 MauiVideoPlayer 型の PlatformView プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。

Android

次のコード例は、Android の UpdateIsLooping メソッドでビデオ ループを有効にする方法を示しています。

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 メソッドは、OnPrepared コールバックを提供するオブジェクトとして MauiVideoPlayer を設定します。 このコールバックは、MediaPlayer.IsLooping プロパティに Video.IsLooping プロパティの値を設定します。

iOS と Mac Catalyst

次のコード例は、iOS および Mac Catalyst の UpdateIsLooping メソッドでビデオ ループを有効にする方法を示しています。

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 オーバーライドは、AVPlayerItem.DidPlayToEndTimeNotification 通知のオブザーバーを削除する DestroyPlayedToEndObserver メソッドを呼び出し、NSObjectDispose メソッドを呼び出します。

Windows

次のコード例は、Windows で UpdateIsLooping メソッドでビデオ ループをできるようにする方法を示しています。

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

ビデオ ループを有効にするには、UpdateIsLooping メソッドで MediaPlayerElement.MediaPlayer.IsLoopingEnabled プロパティを Video.IsLooping プロパティの値に設定します。

カスタム トランスポート コントロール

ビデオプレーヤーのトランスポート コントロールには、動画の再生、一時停止、停止ボタンがあります。 これらのボタンは通常、文字ではなくよく見られるアイコンで分かるようになっています。また、再生ボタンと一時停止ボタンは通常、1 つのボタンです。

既定では、Video コントロールは各プラットフォームでサポートするトランスポート コントロールを表示します。 ただし、AreTransportControlsEnabled プロパティを false に設定すると、これらのコントロールを表示しません。 その後、動画再生をプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。

独自のトランスポート コントロールを実装するには、クラスが Video ビデオの再生、一時停止、または停止をネイティブ ビューに通知し、ビデオ再生の現在の状態を把握できるようにする必要があります。 Video クラスは、対応するイベントを引き起こす PlayPauseStop という名前のメソッドを定義し、コマンドを 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 プロパティを定義します。 このプロパティは、ビデオの再生を開始、一時停止、停止した位置を表します。

PlayPauseStop メソッドの最後の行は、コマンドと VideoHandler に関連付けられたデータを送信します。 VideoHandlerCommandMapper は、コマンドを受信したときに実行するアクションにコマンド名をマップします。 たとえば、VideoHandlerPlayRequested コマンドを受け取ると、MapPlayRequested メソッドを実行します。 この方法の利点は、クロスプラットフォーム コントロール イベントのサブスクライブとサブスクライブ解除をネイティブ ビューで行う必要がなくなることです。 さらに、サブクラス化せずにクロスプラットフォーム コントロール コンシューマーによってコマンド マッパーを変更できるため、簡単にカスタマイズできます。 CommandMapper の詳細については、「コマンド マッパーの作成」をご覧ください。

Android、iOS、Mac Catalyst での MauiVideoPlayer 実装には、PlayRequestedPauseRequestedStopRequested コマンド送信を制御する Video に応答して実行される PlayRequestedPauseRequestedStopRequested メソッドがあります。 各メソッドは、ネイティブ ビューでメソッドを呼び出して、ビデオの再生、一時停止、または停止を行います。 たとえば、次のコードは、iOS および Mac Catalyst の PlayRequestedPauseRequestedStopRequested メソッドを示しています。

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

3 つの各メソッドは、コマンドで送信されたデータを使用して、ビデオが再生、一時停止、または停止された位置をログに記録します。

このメカニズムにより、Video コントロールでPlayPauseまたはStop メソッドが呼び出されると、そのネイティブ ビューでビデオの再生、一時停止、または停止が指示され、ビデオが再生、一時停止、または停止された位置がログに記録されます。 これはすべて分離されたアプローチを使用して行われます。ネイティブ ビューでクロスプラットフォーム イベントをサブスクライブする必要はありません。

ビデオの状態

再生、一時停止、停止などの機能を実装するだけでは、トランスポート コントロールをサポートするためには不十分です。 多くの場合、再生と一時停止の機能は同じボタンで実装され、動画が現在再生中か一時停止中かを示すために外観が変化します。 さらに、ビデオがまだ読み込まれていない場合、ボタンは有効にすべきではありません。

これらの要件は、ビデオ プレーヤーの現在の状態、つまり再生中か一時停止中か、またはまだ再生準備ができていないかということが示される必要があるということを、意味します。 この状態は、列挙型で表すことができます:

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

通常、読み取り専用のバインド可能なプロパティには、クラス内からの設定を可能にするため、Status プロパティに対するプライベート set アクセサーが存在します。 ただし、ハンドラーによってサポートされる View の派生物の場合、プロパティはクラスの外部、ただしコントロールのハンドラーによってのみ設定する必要があります。

このため、IVideoController.Status という名前の別のプロパティが定義されます。 これは、明示的なインターフェイスの実装であり、Video クラスによって実装された IVideoController インターフェイスによって可能となります。

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

このインターフェイスにより、Video の外部のクラスが IVideoController インターフェイスを参照してStatus プロパティを設定することが可能になります。 このプロパティは他のクラスやハンドラーからも設定できますが、誤って設定されることはほとんどありません。 最も重要なこととして、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 イベント ハンドラーは 10 分の 1 秒ごとに実行され、UpdateStatus イベントを発生して UpdateStatus コマンドを呼び出します。

UpdateStatus コマンドがVideo コントロールから ハンドラーに送信されると、ハンドラーのコマンド マッパーによってMapUpdateStatus メソッドが確実に 呼び出されます。

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

MapUpdateStatus メソッドは、ハンドラーの PlatformView プロパティの UpdateStatus メソッドを呼び出します。 MauiVideoPlayer 型の PlatformView プロパティは、各プラットフォームでビデオ プレーヤーの実装を提供するネイティブ ビューをカプセル化します

Android

次のコード例は、Android の UpdateStatus メソッドが 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 プロパティを使い、Video オブジェクトの Status プロパティを IVideoController にキャストして設定します。

iOS と Mac Catalyst

次のコード例では、iOS および Mac Catalyst の UpdateStatus メソッドが 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 プロパティを設定するには、AVPlayer の 2つのプロパティ (AVPlayerStatus 型の Status プロパティと AVPlayerTimeControlStatus 型の TimeControlStatus プロパティ) にアクセスする必要があります。 次に、IVideoController にキャストすることで、Video オブジェクトに Status プロパティを設定することができます。

Windows

次のコード例は、Windows の UpdateStatus メソッドが 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 プロパティの値を決定します。 次に、IVideoController にキャストすることで、Video オブジェクトに Status プロパティを設定することができます。

位置バー

各プラットフォームで実装されているトランスポート コントロールには、位置バーが含まれます。 このバーはスライダーまたはスクロール バーに似ており、ビデオの合計時間内の現在の位置を示します。 ユーザーは位置バーを操作して、ビデオ内の新しい位置へと前後に移動することができます。

独自の位置バーを実装するには、Video クラスがビデオの再生時間と、その再生時間内における現在の位置を知っている必要があります。

Duration

Video コントロールがカスタムの位置バーをサポートするために必要な情報の 1 つに、ビデオの再生時間があります。 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); }
        }
        ...
    }
}

通常、読み取り専用のバインド可能なプロパティには、クラス内からの設定を可能にするため、Duration プロパティに対するプライベート set アクセサーが存在します。 ただし、ハンドラーによってサポートされる View の派生物の場合、プロパティはクラスの外部、ただしコントロールのハンドラーによってのみ設定する必要があります。

Note

Duration バインド可能なプロパティのプロパティ変更イベント ハンドラーは、「終了までの時間の計算」で説明されている SetTimeToEnd というメソッドを呼び出します。

このため、IVideoController.Duration という名前の別のプロパティが定義されます。 これは、明示的なインターフェイスの実装であり、Video クラスによって実装された IVideoController インターフェイスによって可能となります。

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

このインターフェイスにより、Video の外部のクラスが IVideoController インターフェイスを参照してDuration プロパティを設定することが可能になります。 このプロパティは他のクラスやハンドラーからも設定できますが、誤って設定されることはほとんどありません。 最も重要なこととして、Duration プロパティはデータ バインドを通じて設定することはできません。

Video コントロールの Source プロパティが設定された直後は、ビデオの再生時間は表示されません。 ネイティブ ビューが再生時間を決定するには、その前に、部分的にビデオをダウンロードする必要があります。

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 クラスは 1 秒間に 10 回呼び出される UpdateStatus メソッドで再生時間を取得します:

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 メソッドを呼び出し、メインスレッド上で Video オブジェクトの Duration プロパティを IVideoController にキャストして設定します。 これは、MediaPlayerElement.MediaPlayer.MediaOpened イベントがバックグラウンド スレッドで処理されるために必要です。 メイン スレッドでコードを実行する方法の詳細については、「.NET MAUI UI スレッドでスレッドを作成する」をご覧ください。

配置

また、Video コントロールには Position プロパティが必要で、ビデオの再生に合わせてゼロから Duration まで増加します。 Video クラスは、パブリック getset アクセサーを持つバインド可能なプロパティとしてこのプロパティを実装します。

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 アクセサーは、ユーザーが位置バーを操作すると、ビデオの位置を前方または後方に動かして反応します。

Note

Position バインド可能なプロパティのプロパティ変更イベント ハンドラーは、「終了までの時間の計算」で説明されている SetTimeToEnd というメソッドを呼び出します。

Android、iOS、Mac Catalyst では、現在の位置を取得するプロパティには get アクセサーのみが含まれます。 代わりに、位置を設定する Seek メソッドを使用できます。 これは、固有の問題がある単一の Position プロパティを使用するよりも、より賢明なアプローチです。 動画が再生されると、Position プロパティは新しい位置を反映するために継続的に更新される必要があります。 しかし、Position プロパティのほとんどの変更が、ビデオプレーヤーをビデオの新しい位置に移動させる原因になることは望ましくありません。 このような処理の結果、ビデオ プレーヤーは Position プロパティの最後の値までシークする処理で応答するので、ビデオは進みません。

Position プロパティを get アクセサーと set アクセサーで実装することの難しさにもかかわらず、この方法が使われるのは、データ バインディングを利用できるからです。 Video コントロールの Position プロパティは、位置の表示と新しい位置の検索の両方に使用される Slider にバインドできます。 ただし、Position プロパティを実装する際には、フィードバック ループを避けるためにいくつかの事前の注意が必要です。

Android

Android では、VideoView.CurrentPosition プロパティがビデオの現在位置を示します。 MauiVideoPlayer クラスは、Duration プロパティを設定すると同時に、UpdateStatus メソッドで Position プロパティを設定します。

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 メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。 UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達した同じ位置に移動してしまいます。 このフィードバック ループを避けるため、UpdatePosition は、Position プロパティと VideoView の現在位置との差が 1 秒を超えたときのみ、VideoView オブジェクトの Seek メソッドを呼び出します。

iOS と Mac Catalyst

iOS と Mac Catalyst では、AVPlayerItem.CurrentTime プロパティはビデオの現在の位置を示します。 MauiVideoPlayer クラスは、Duration プロパティを設定すると同時に、UpdateStatus メソッドで Position プロパティを設定します。

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 メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。 UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達した同じ位置に移動してしまいます。 このフィードバック ループを避けるため、UpdatePosition は、Position プロパティと AVPlayer の現在位置との差が 1 秒を超えたときのみ、AVPlayer オブジェクトの Seek メソッドを呼び出します。

Windows

Windows では、MediaPlayerElement.MedaPlayer.Position プロパティはビデオの現在の位置を示します。 MauiVideoPlayer クラスは、Duration プロパティを設定すると同時に、UpdateStatus メソッドで Position プロパティを設定します。

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 メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。 UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達した同じ位置に移動してしまいます。 このフィードバック ループを避けるため、UpdatePosition は、Position プロパティと MediaPlayerElement の現在位置との差が 1 秒を超えたときのみ、MediaPlayerElement.MediaPlayer.Position プロパティを設定します。

終了までの時間の計算

ビデオ プレーヤーにビデオの残り時間が表示されることがあります。 この値は、ビデオの開始時にはビデオの再生時間から始まり、ビデオの終了時には 0 まで減少します。

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 プロパティのプロパティ変更イベント ハンドラーから呼び出されます。

カスタム位置バー

カスタム位置バーは、TimeSpan 型の Duration プロパティと Position プロパティを含む Slider から派生したクラスを作成することで実装できます。

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 プロパティのプロパティ変更イベント ハンドラーは、SliderMaximum プロパティを TimeSpan 値の TotalSeconds プロパティに設定します。 同様に、Position プロパティのプロパティ変更イベント ハンドラーは、SliderValue プロパティを設定します。 これは、PositionSlider の位置を Slider が追跡するメカニズムです。

PositionSlider が基盤となる Slider から更新されるのは、ユーザーが Slider を操作してビデオを新しい位置に進めたり戻したりするというシナリオのみです。 これは、PositionSlider コンストラクターの PropertyChanged ハンドラーで検出されます。 このイベント ハンドラーでは、Value プロパティの変更をチェックし、それが Position プロパティと異なる場合は、Value プロパティから Position プロパティが設定されます。

ハンドラーの登録

カスタム コントロールとそのハンドラーは、アプリを使用する前に登録する必要があります。 これは、アプリ プロジェクトの MauiProgram クラス内の CreateMauiApp メソッド (アプリのクロスプラットフォーム エントリ ポイント) で発生する必要があります。

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

ハンドラは ConfigureMauiHandlersAddHandler メソッドで登録されます。 AddHandler メソッドの最初の引数は、クロスプラットフォーム コントロール型で、2 番目の引数はそのハンドラー型です。

クロスプラットフォーム コントロールの使用

ハンドラーをアプリに登録した後、クロスプラットフォーム コントロールを使用できます。

Web ビデオの再生

次の例に示すように、 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を表す文字列を UriVideoSource に変換しています。 十分な量のデータがダウンロードされ、バッファリングされると、ビデオは読み込みと再生を開始します。 各プラットフォームで、トランスポート コントロールが使用されていない場合はフェード アウトしますが、ビデオをタップすることで表示を復元することができます。

ビデオ リソースの再生

アプリの ResourcesRaw フォルダーに 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 イベント ハンドラーで .NET MAUI の MediaPicker クラスを使用することにより、ユーザーはデバイスからビデオファイルを選択できます。 選択したビデオ ファイルは、FileVideoSource オブジェクトとしてカプセル化され、Video コントロールの Source プロパティとして設定されます。 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" />

AutoPlayAreTransportControlsEnabledfalseに設定すると、ビデオの再生が開始されず、再生を開始する方法がなくなります。 このシナリオでは、分離コード ファイルから 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 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 を定義しています。 ボタンの外観は、アイコンとテキストで構成されるボタンを作成するために、Unicode 文字とそのテキストに相当するものを使用して定義されます。

再生ボタンと一時停止ボタンのスクリーンショット。

ビデオが再生されると、再生ボタンが一時停止ボタンに更新されます:

一時停止ボタンと停止ボタンのスクリーンショット。

UI には、ビデオの読み込み中に表示される 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();
    }
    ...
}

カスタム位置バー

次の例は、XAML で使用されるカスタム位置バー PositionSlider を示しています。

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

Video オブジェクトの Position プロパティは、PositionSliderPosition プロパティにバインドされますが、Video.Position プロパティは MauiVideoPlayer.UpdateStatus メソッドによって変更されるため、パフォーマンスの問題は発生しません。 各プラットフォームは 1 秒間に 10 回しか呼び出されません。 さらに、2 つの Label オブジェクトは、Video オブジェクトの Position プロパティ値と TimeToEnd プロパティ値を表示します。

ネイティブ ビュー クリーンアップ

各プラットフォームのハンドラー実装は、DisconnectHandler 実装をオーバーライドします。この実装は、イベントのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行するために使用されます。 ただし、このオーバーライドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な場所から自分で呼び出す必要があります。 これは多くの場合、Video コントロールを含むページから移動し、ページの Unloaded イベントが発生する場合に発生します。

ページの Unloaded イベントのイベント ハンドラーは、XAML で登録できます。

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

Unloaded イベントのイベント ハンドラーは、Handler インスタンスで DisconnectHandler メソッドを呼び出すことができます。

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();を呼び出すことによって実現できます。

さらに、特定の IView からハンドラーを切断する DisconnectHandlers 拡張メソッドがあります。

video.DisconnectHandlers();

切断すると、完了するか、手動ポリシーを設定してあるコントロールに到達するまで、DisconnectHandlers メソッドがコントロール ツリー内を伝達されていきます。