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

Browse sample. サンプルを参照する

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

  • リモート動画を示す URL。
  • アプリに埋め込まれたファイルであるリソース。
  • デバイスの動画ライブラリからのファイル。

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

Screenshot of video playback on iOS.Screenshot of video playback using custom transport controls on iOS.

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

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

Video handler architecture.

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 型をハンドラー内で使用し、すべてのプラットフォームで使用できるようにするために必要です。 別の方法としては、条件付きコンパイルを使って、プラットフォームごとに一度 PlatformView プロパティを定義する必要があります。

プロパティ マッパーの作成

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

PropertyMapper は .NET MAUI のジェネリック ViewHandler クラスで定義されており、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 クラスで定義されており、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 を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールからコマンドが送信されると、基になるネイティブ ビューが必要に応じて操作されるようになります。 この方法の利点は、クロスプラットフォーム コントロール イベントのサブスクライブとサブスクライブ解除をネイティブ ビューで行う必要がなくなることです。 さらに、サブクラス化せずにクロスプラットフォーム コントロール コンシューマーによってコマンド マッパーを変更できるため、簡単にカスタマイズできます。

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

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

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

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

マッパーを含む 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 クラスから派生する必要があります。これには、次の 2 つの型引数が必要です。

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

重要

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

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

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

重要

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

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

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

Android

Android では VideoView でビデオが再生されます。 ただし、ここでは、VideoView はネイティブ ビューをハンドラーから分離した状態で維持するために、MauiVideoPlayer 型にカプセル化されています。 次の例は、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 クラスから派生し、ジェネリック Video 引数はクロス プラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は VideoView ネイティブ ビューをカプセル化する型を指定します。

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateTransportControlsEnabled();
    }

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

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

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

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

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

Android では、MauiVideoPlayer クラスは VideoView をカプセル化し、ネイティブ ビューをハンドラーから分離して保持します。

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

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

        public MauiVideoPlayer(Context context, Video video) : base(context)
        {
            _context = context;
            _video = video;

            SetBackgroundColor(Color.Black);

            // Create a RelativeLayout for sizing the video
            RelativeLayout relativeLayout = new RelativeLayout(_context)
            {
                LayoutParameters = new CoordinatorLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
                {
                    Gravity = (int)GravityFlags.Center
                }
            };

            // Create a VideoView and position it in the RelativeLayout
            _videoView = new VideoView(context)
            {
                LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
            };

            // Add to the layouts
            relativeLayout.AddView(_videoView);
            AddView(relativeLayout);

            // Handle events
            _videoView.Prepared += OnVideoViewPrepared;
        }
        ...
    }
}

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

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

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

VideoHandler は、クロス プラットフォーム コントロール型を指定するジェネリック Video 引数と、AVPlayerAVPlayerViewController ネイティブ ビューをカプセル化する型を指定する MauiVideoPlayer 引数を使用して、ViewHandler クラスから派生します。

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler?.PlatformView.UpdateTransportControlsEnabled();
    }

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

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

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

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

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

iOS および Mac Catalyst では、MauiVideoPlayer クラスは AVPlayer 型と AVPlayerViewController 型をカプセル化して、ネイティブ ビューをハンドラーから分離して保持します。

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

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

        public MauiVideoPlayer(Video video)
        {
            _video = video;

            _playerViewController = new AVPlayerViewController();
            _player = new AVPlayer();
            _playerViewController.Player = _player;
            _playerViewController.View.Frame = this.Bounds;

#if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER
            // On iOS 16 and Mac Catalyst 16, for Shell-based apps, the AVPlayerViewController has to be added to the parent ViewController, otherwise the transport controls won't be displayed.
            var viewController = WindowStateManager.Default.GetCurrentUIViewController();

            // If there's no view controller, assume it's not Shell and continue because the transport controls will still be displayed.
            if (viewController?.View is not null)
            {
                // Zero out the safe area insets of the AVPlayerViewController
                UIEdgeInsets insets = viewController.View.SafeAreaInsets;
                _playerViewController.AdditionalSafeAreaInsets = new UIEdgeInsets(insets.Top * -1, insets.Left, insets.Bottom * -1, insets.Right);

                // Add the View from the AVPlayerViewController to the parent ViewController
                viewController.View.AddSubview(_playerViewController.View);
            }
#endif
            // Use the View from the AVPlayerViewController as the native control
            AddSubview(_playerViewController.View);
        }
        ...
    }
}

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 クラスから派生します。クロスプラットフォーム コントロール型を指定する汎用 Video 引数と、MediaPlayerElement ネイティブ ビューをカプセル化する型を指定する MauiVideoPlayer 引数を伴います。

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
    {
        handler.PlatformView?.UpdateTransportControlsEnabled();
    }

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

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

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

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

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

public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
    ...
    public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
    {
        handler.PlatformView?.UpdateStatus();
    }

    public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PlayRequested(position);
    }

    public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.PauseRequested(position);
    }

    public static void MapStopRequested(VideoHandler handler, Video video, object? args)
    {
        if (args is not VideoPositionEventArgs)
            return;

        TimeSpan position = ((VideoPositionEventArgs)args).Position;
        handler.PlatformView?.StopRequested(position);
    }
    ...
}

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

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

MediaOpened イベントからの登録を解除する他に、Dispose オーバーライドは ネイティブ ビューのクリーンアップも実行します。

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

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 オブジェクトに渡されます。 次のコード例は、Source プロパティが UriVideoSource 型の場合に UpdateSource メソッドで処理する方法を示しています。

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 では、ビデオ再生ページから移動した後もビデオの再生が続く場合があります。 ビデオを停止するには、ReplaceCurrentItemWithPlayerItemDispose オーバーライドで null に設定します。

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

Windows

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

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 が設定されると、OnMediaPlayerMediaOpened イベント ハンドラー メソッドが MediaPlayerElement.MediaPlayer.MediaOpened イベントに対して登録されます。 このイベント ハンドラーは、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 メソッドを呼び出します。 PlatformView プロパティは MauiVideoPlayer 型であり、各プラットフォームでのビデオ プレーヤーの実装を提供するネイティブ ビューを表します。

Android

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

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 メソッドを使用して再生するビデオを指定し、アプリのパッケージ名とビデオのファイル名を組み合わせた文字列引数を指定します。

リソース ビデオ ファイルはパッケージの assets フォルダーに格納され、アクセスするにはコンテンツ プロバイダーが必要です。 コンテンツ プロバイダーは、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

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

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

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

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 プロパティは MediaSource オブジェクトに設定され、ms-appx:/// のプレフィックスが付いたビデオ リソースのパスを持つ Uri を初期化します。

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

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

Android

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

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

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

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

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

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 型のオブジェクトを処理する場合、ビデオの filename は 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 メソッドを呼び出します。 PlatformView プロパティは MauiVideoPlayer 型であり、各プラットフォームでのビデオ プレーヤーの実装を提供するネイティブ ビューを表します。

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 クラスは、対応するイベントを発生させ、VideoHandler にコマンドに送信する PlayPauseStop という名前のメソッドを定義します。

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        public event EventHandler<VideoPositionEventArgs> PlayRequested;
        public event EventHandler<VideoPositionEventArgs> PauseRequested;
        public event EventHandler<VideoPositionEventArgs> StopRequested;

        public void Play()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            PlayRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.PlayRequested), args);
        }

        public void Pause()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            PauseRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.PauseRequested), args);
        }

        public void Stop()
        {
            VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
            StopRequested?.Invoke(this, args);
            Handler?.Invoke(nameof(Video.StopRequested), args);
        }
    }
}

VideoPositionEventArgs クラスは、そのコンストラクターを通じて設定できる Position プロパティを定義します。 このプロパティは、動画再生が開始、一時停止または停止された位置を表します。

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

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

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 コントロールで Play メソッド、Pause メソッドまたは Stop メソッドが呼び出されると、そのネイティブ ビューで動画の再生、一時停止または停止が指示され、動画が再生、一時停止または停止された位置がログに記録されます。 これはすべて、分離されたアプローチを使用して行われます。ネイティブ ビューでクロス プラットフォーム イベントをサブスクライブする必要はありません。

動画の状態

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

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

public enum VideoStatus
{
    NotReady,
    Playing,
    Paused
}

Video クラスにより、VideoStatus 型の Status という名前の、読み取り専用のバインド可能なプロパティが定義されます。 このプロパティは、コントロールのハンドラーからのみ設定されるので、読み取り専用として定義されています。

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 イベント ハンドラーは 1/10 秒ごとに実行され、UpdateStatus イベントが発生して UpdateStatus コマンドが呼び出されます。

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

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

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

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 クラスは、TimeSpan 型の Duration という名前の、読み取り専用のバインド可能なプロパティを定義します。 このプロパティは、コントロールのハンドラーからのみ設定されるので、読み取り専用として定義されています。

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

配置

Video の制御には、ビデオの再生時に 0 から Duration まで増加する Position プロパティも必要です。 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 というバインド可能プロパティのプロパティ変更イベント ハンドラーは、「Calculating time to end」で説明されている SetTimeToEnd というメソッドを呼び出します。

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

getset アクセサーを使用して Position プロパティを実装することは困難ですが、データ バインディングを利用できるため、このアプローチが使用されます。 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 メソッドは、ほとんどのプロパティの変更に対して何も実行する必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達していた同じ位置まで移動してしまいます。 このフィードバック ループを避けるために、Position プロパティと VideoView の現在の位置との違いが 1 秒を超える場合にのみ、VideoView オブジェクトで UpdatePositionSeek メソッドを呼び出します。

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

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 メソッドは、ほとんどのプロパティの変更に対して何も実行する必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達していた同じ位置まで移動してしまいます。 このフィードバック ループを避けるために、Position プロパティと MediaPlayerElement の現在の位置との違いが 1 秒を超える場合にのみ、UpdatePositionMediaPlayerElement.MediaPlayer.Position プロパティを設定します。

終了までの時間の計算

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

Video クラスには、DurationPosition プロパティへの変更に基づいて計算される読み取り専用 TimeToEnd プロパティが含まれています。

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 プロパティと異なる場合は、Position プロパティが Value プロパティから設定されます。

ハンドラーの登録

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

using VideoDemos.Controls;
using VideoDemos.Handlers;

namespace VideoDemos;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler(typeof(Video), typeof(VideoHandler));
            });

        return builder.Build();
    }
}

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

ビデオ リソースを再生する

MauiAsset ビルド アクションを含むアプリの Resources\Raw フォルダーに埋め込まれているビデオ ファイルは、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 文字とそのテキストに相当するものを使用して定義されます。

Screenshot of play and pause buttons.

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

Screenshot of pause and stop buttons.

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

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

各プラットフォームのハンドラー実装は、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 での後方ナビゲーションで動画の再生が停止します。