アプリの標準的な要件は、ビデオを再生する機能です。 この記事では、.NET マルチプラットフォーム アプリ UI (.NET MAUI) クロスプラットフォーム Video コントロールを作成する方法について説明します。このコントロールは、ハンドラーを使用して、クロスプラットフォームコントロール APIを、動画を再生する Android、iOS、Mac Catalyst のネイティブビューにマッピングします。 このコントロールは、3 つのソースからビデオを再生できます。
- リモート ビデオを表す URL。
- リソース。アプリに埋め込まれたファイル。
- デバイスのビデオ ライブラリからのファイル。
ビデオ コントロールには、「トランスポート コントロール」 (ビデオを再生および一時停止するためのボタン) と、ビデオの進行状況を表示したりユーザーが別の場所にすばやくスキップできたりする配置バーが必要です。
Video コントロールは、プラットフォームによって提供されるトランスポート コントロールと配置バーのどちらかを使えます。または、ご自身でカスタムのトランスポート コントロールと配置バーを提供できます。 次のスクリーンショットは、カスタム トランスポート コントロールがある場合とない場合の iOS 上のコントロールを示しています。
より洗練されたビデオ プレーヤーには、音量調整や、電話がかかってきたときにビデオを中断するメカニズム、再生中に画面をアクティブ状態に保つ方法など、いくつかの追加機能が備わっています。
次の図に、 Video コントロールのアーキテクチャを示します:
Video クラスは、コントロールのクロスプラットフォーム API を提供します。 クロスプラットフォーム API からネイティブ ビュー API へのマッピングは、各プラットフォーム上の VideoHandler クラスによって実行され、 Video クラスがMauiVideoPlayer クラスにマップされます。 iOS および Mac Catalyst では、MauiVideoPlayer クラスは AVPlayer 型を使用してビデオ再生を提供します。 Android では、MauiVideoPlayer クラスは VideoView 型を使用してビデオ再生を提供します。 Windowsでは、 MauiVideoPlayer クラスは MediaPlayerElement 型を使用してビデオ再生を提供します。
重要
.NET MAUI は、インターフェイスを介して、そのハンドラーをクロスプラットフォーム コントロールから切り離します。 これにより、Comet や Fabulous などの実験的なフレームワークは、.NET MAUI のハンドラーを引き続き使用しながら、インターフェイスを実装する独自のクロスプラットフォーム コントロールを提供できます。 クロスプラットフォーム コントロールのインターフェイスの作成は、同様の目的やテスト目的で、ハンドラーをそのクロスプラットフォーム コントロールから切り離す必要がある場合にのみ必要です。
ハンドラーによってプラットフォーム実装が提供される、クロスプラットフォーム .NET MAUI カスタム コントロールを作成するプロセスは次のとおりです:
- コントロールのパブリック API を提供するクロスプラットフォーム コントロールのクラスを作成します。 詳細については、「クロスプラットフォームコントロールの作成」を参照してください。
- 必要なクロスプラットフォーム型を追加作成します。
-
partialハンドラー クラスを作成します。 詳細については、「ハンドラーを作成する」をご覧ください。 - ハンドラー クラスで、PropertyMapper ディクショナリを作成します。これは、クロスプラットフォームのプロパティ変更が発生したときに実行するアクションを定義します。 プロパティの詳細については、「プロパティマッパーの作成」を参照してください。
- 必要に応じて、ハンドラー クラスで CommandMapper ディクショナリを作成します。このディクショナリは、クロスプラットフォーム コントロールがクロスプラットフォーム コントロールを実装するネイティブ ビューに命令を送信するときに実行するアクションを定義します。 詳細については、「コマンドマッパーの作成」を参照してください。
- クロスプラットフォーム コントロールを実装するネイティブ ビューを作成する
partialハンドラー クラスを、プラットフォームごとに作成します。 詳細については、「プラットフォーム コントロールを作成する」をご覧ください。 - アプリの ConfigureMauiHandlers クラスの AddHandler メソッドと
MauiProgramメソッドを使用してハンドラーを登録します。 詳細については、「ハンドラーを登録する」をご覧ください。
その後、クロスプラットフォーム コントロールを使用できます。 詳細については、「クロスプラットフォームコントロールの使用」を参照してください。
クロスプラットフォーム コントロールを作成する
クロスプラットフォーム コントロールを作成するには、View から派生するクラスを作成する必要があります。
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
public static readonly BindableProperty AreTransportControlsEnabledProperty =
BindableProperty.Create(nameof(AreTransportControlsEnabled), typeof(bool), typeof(Video), true);
public static readonly BindableProperty SourceProperty =
BindableProperty.Create(nameof(Source), typeof(VideoSource), typeof(Video), null);
public static readonly BindableProperty AutoPlayProperty =
BindableProperty.Create(nameof(AutoPlay), typeof(bool), typeof(Video), true);
public static readonly BindableProperty IsLoopingProperty =
BindableProperty.Create(nameof(IsLooping), typeof(bool), typeof(Video), false);
public bool AreTransportControlsEnabled
{
get { return (bool)GetValue(AreTransportControlsEnabledProperty); }
set { SetValue(AreTransportControlsEnabledProperty, value); }
}
[TypeConverter(typeof(VideoSourceConverter))]
public VideoSource Source
{
get { return (VideoSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public bool AutoPlay
{
get { return (bool)GetValue(AutoPlayProperty); }
set { SetValue(AutoPlayProperty, value); }
}
public bool IsLooping
{
get { return (bool)GetValue(IsLoopingProperty); }
set { SetValue(IsLoopingProperty, value); }
}
...
}
}
コントロールは、ハンドラーによってアクセスされ、コンシューマーを制御するパブリック API を提供する必要があります。 クロスプラットフォーム コントロールは、画面にレイアウトとビューを配置するために使用されるビジュアル要素を表す View から派生する必要があります。
ハンドラーを作成する
クロスプラットフォーム コントロールを作成したら、ハンドラーの partial クラスを作成する必要があります。
#if IOS || MACCATALYST
using PlatformView = VideoDemos.Platforms.MaciOS.MauiVideoPlayer;
#elif ANDROID
using PlatformView = VideoDemos.Platforms.Android.MauiVideoPlayer;
#elif WINDOWS
using PlatformView = VideoDemos.Platforms.Windows.MauiVideoPlayer;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif
using VideoDemos.Controls;
using Microsoft.Maui.Handlers;
namespace VideoDemos.Handlers
{
public partial class VideoHandler
{
}
}
ハンドラー クラスは部分クラスであり、その実装は追加の部分クラスを使用して各プラットフォームで完了します。
using 条件ステートメントは、各プラットフォームで PlatformView 型を定義します。 Android、iOS、Mac Catalyst、Windows では、ネイティブ ビューはカスタム MauiVideoPlayer クラスによって提供されます。 最後の部分的 using ステートメントが、PlatformView が System.Objectと等しいことを定義しています。 これは、PlatformView 型をハンドラー内で使用してすべてのプラットフォームで使用できるようにするために必要です。 別の方法としては、条件付きコンパイルを使用して、プラットフォームごとに 1 回 PlatformView プロパティを定義する必要があります。
プロパティ マッパーを作成する
通常、各ハンドラーはプロパティ マッパーを提供します。これは、クロスプラットフォーム コントロールでプロパティの変更が発生したときに実行するアクションを定義します。
PropertyMapper 型は、クロスプラットフォーム コントロールのプロパティを関連するアクションにマッピングする Dictionary です。
PropertyMapper は .NET MAUI の ViewHandler<TVirtualView,TPlatformView> クラスで定義されており、2 つのジェネリック引数を指定する必要があります。
- View から派生するクロスプラットフォーム コントロールのクラス。
- ハンドラーのクラス。
次のコード例は、VideoHandler 定義により拡張された PropertyMapper クラスを示しています。
public partial class VideoHandler
{
public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
{
[nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
[nameof(Video.Source)] = MapSource,
[nameof(Video.IsLooping)] = MapIsLooping,
[nameof(Video.Position)] = MapPosition
};
public VideoHandler() : base(PropertyMapper)
{
}
}
PropertyMapper は Dictionary で、そのキーは string で、その値はジェネリック Action です。
string はクロスプラットフォーム コントロールのプロパティ名を表し、Action は、引数としてハンドラーとクロスプラットフォーム コントロールを必要とする static メソッドを表します。 例えば、MapSource メソッドのシグネチャは public static void MapSource(VideoHandler handler, Video video) です。
各プラットフォーム ハンドラーは、ネイティブ ビュー API を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールでプロパティが設定されると、基になるネイティブ ビューが必要に応じて更新されます。 このアプローチの利点は、クロスプラットフォーム コントロール コンシューマーがサブクラス化せずにプロパティ マッパーを変更できるため、クロスプラットフォーム コントロールを簡単にカスタマイズできることです。
コマンド マッパーを作成する
各ハンドラーは、クロスプラットフォーム コントロールがネイティブ ビューにコマンドを送信するときに実行するアクションを定義するコマンド マッパーを提供することもできます。 コマンド マッパーはプロパティ マッパーに似ていますが、追加のデータを渡すことができます。 このコンテキストでは、コマンドは命令であり、必要に応じてそのデータがネイティブ ビューに送信されます。
CommandMapper 型は Dictionary で、クロスプラットフォーム コントロール メンバーを関連するアクションにマッピングします。
CommandMapper は .NET MAUI の ViewHandler<TVirtualView,TPlatformView> クラスで定義されており、2 つのジェネリック引数を指定する必要があります。
- View から派生するクロスプラットフォーム コントロールのクラス。
- ハンドラーのクラス。
次のコード例は、VideoHandler 定義により拡張された CommandMapper クラスを示しています。
public partial class VideoHandler
{
public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
{
[nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
[nameof(Video.Source)] = MapSource,
[nameof(Video.IsLooping)] = MapIsLooping,
[nameof(Video.Position)] = MapPosition
};
public static CommandMapper<Video, VideoHandler> CommandMapper = new(ViewCommandMapper)
{
[nameof(Video.UpdateStatus)] = MapUpdateStatus,
[nameof(Video.PlayRequested)] = MapPlayRequested,
[nameof(Video.PauseRequested)] = MapPauseRequested,
[nameof(Video.StopRequested)] = MapStopRequested
};
public VideoHandler() : base(PropertyMapper, CommandMapper)
{
}
}
CommandMapper は Dictionary で、そのキーは string で、その値はジェネリック Action です。
string は、クロスプラットフォーム コントロールのコマンド名を表し、Action は引数としてハンドラー、クロスプラットフォーム コントロール、および省略可能なデータを必要とする static メソッドを表します。 例えば、MapPlayRequested メソッドのシグネチャは public static void MapPlayRequested(VideoHandler handler, Video video, object? args) です。
各プラットフォーム ハンドラーは、ネイティブ ビュー API を操作するアクションの実装を提供する必要があります。 これにより、クロスプラットフォーム コントロールからコマンドが送信されると、基になるネイティブ ビューが必要に応じて操作されるようになります。 この方法の利点は、クロスプラットフォーム コントロール イベントのサブスクライブとサブスクライブ解除をネイティブ ビューで行う必要がなくなることです。 さらに、サブクラス化せずにクロスプラットフォーム コントロール コンシューマーによってコマンド マッパーを変更できるため、簡単にカスタマイズできます。
プラットフォーム コントロールの作成
ハンドラーのマッパーを作成した後、すべてのプラットフォームでハンドラーの実装を提供する必要があります。 これを実現するには、プラットフォーム フォルダーの子フォルダーに部分クラス ハンドラーの実装を追加します。 または、ファイル名ベースのマルチターゲット、フォルダーベースのマルチターゲット、またはその両方をサポートするようにプロジェクトを構成することもできます。
サンプル アプリは、ファイル名ベースのマルチターゲットをサポートするように構成されているため、ハンドラー クラスはすべて 1 つのフォルダーに配置されます。
マッパーを含む VideoHandler クラスには、VideoHandler.cs という名前が付けられます。 そのプラットフォームの実装は、VideoHandler.Android.cs、VideoHandler.MaciOS.cs、VideoHandler.Windows.cs ファイルに含まれています。 このファイル名ベースのマルチターゲットは、次の XML を <Project> ノードの子としてプロジェクト ファイルに追加することによって構成されます。
<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
<Compile Remove="**\*.Android.cs" />
<None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- iOS and Mac Catalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
<Compile Remove="**\*.MaciOS.cs" />
<None Include="**\*.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
<Compile Remove="**\*.Windows.cs" />
<None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
マルチターゲットの構成の詳細については、「マルチターゲットの構成」をご覧ください。
各プラットフォーム ハンドラー クラスは部分クラスで、 ViewHandler<TVirtualView,TPlatformView> クラスから派生する必要があります。これには 2 つの型引数が必要です。
- View から派生するクロスプラットフォーム コントロールのクラス。
- プラットフォームでクロスプラットフォーム コントロールを実装するネイティブ ビューの型。 これは、ハンドラー内の
PlatformViewプロパティの型と同じである必要があります。
重要
ViewHandler<TVirtualView,TPlatformView> クラスは、VirtualView プロパティと PlatformView プロパティを提供します。 VirtualView プロパティは、ハンドラーからクロスプラットフォーム コントロールにアクセスするために使用されます。 PlatformView プロパティは、クロスプラットフォーム コントロールを実装する各プラットフォームのネイティブ ビューにアクセスするために使用されます。
プラットフォーム ハンドラーの実装はそれぞれ、次のメソッドをオーバーライドする必要があります。
- CreatePlatformView は、クロスプラットフォーム コントロールを実装するネイティブ ビューを作成して返す必要があります。
- ConnectHandler は、ネイティブ ビューの初期化やイベント サブスクリプションの実行など、ネイティブ ビューのセットアップを実行する必要があります。
- DisconnectHandlerは、イベントからのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行する必要があります。
重要
DisconnectHandler メソッドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な場所から自分で呼び出す必要があります。 詳細については、「ネイティブ ビューのクリーンアップ」をご覧ください。
重要
DisconnectHandler メソッドは既定で .NET MAUI によって自動的に呼び出されますが、この動作は変更できます。 詳細については、「 Control ハンドラーの切断」を参照してください。
各プラットフォーム ハンドラーは、マッパー ディクショナリで定義されている各 Action も実装する必要があります。
さらに、各プラットフォーム ハンドラーは、プラットフォームにクロスプラットフォーム コントロールの機能を実装するために、必要に応じてコードを提供する必要もあります。 または、ここで採用されている手法である追加の型として提供することもできます。
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);
}
...
}
}
VideoHandler は ViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は VideoView ネイティブ ビューをカプセル化する型を指定します。
CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。
ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。
DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、Dispose インスタンスで MauiVideoPlayer メソッドを呼び出します。
プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdatePosition();
}
...
}
各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。
プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、MauiVideoPlayer クラスで定義されたメソッドを呼び出します。
Android では、MauiVideoPlayerクラスは、ネイティブ ビューをハンドラーから分離し続けるために VideoView をカプセル化 します。
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
MediaController _mediaController;
bool _isPrepared;
Context _context;
Video _video;
public MauiVideoPlayer(Context context, Video video) : base(context)
{
_context = context;
_video = video;
SetBackgroundColor(Color.Black);
// Create a RelativeLayout for sizing the video
RelativeLayout relativeLayout = new RelativeLayout(_context)
{
LayoutParameters = new CoordinatorLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
{
Gravity = (int)GravityFlags.Center
}
};
// Create a VideoView and position it in the RelativeLayout
_videoView = new VideoView(context)
{
LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
};
// Add to the layouts
relativeLayout.AddView(_videoView);
AddView(relativeLayout);
// Handle events
_videoView.Prepared += OnVideoViewPrepared;
}
...
}
}
Android での.NET MAUI アプリのルート ネイティブ ビューは MauiVideoPlayer であるため、CoordinatorLayout が CoordinatorLayout から派生します。
MauiVideoPlayer クラスは他のネイティブ Android 型から派生することもありますが、一部のシナリオではネイティブ ビューの配置の制御が難しくなる場合があります。
VideoView は CoordinatorLayout に直接追加することができ、必要に応じてレイアウト上での配置が可能です。 ただし、ここでは、Android RelativeLayout が CoordinatorLayout に追加され、VideoView は RelativeLayout に追加されます。 レイアウト パラメーターは RelativeLayout と VideoView の両方に設定され、VideoView がページ中央に配置され、縦横比を維持しながら、使用可能な領域を埋めるように展開します。
コンストラクターは VideoView.Prepared イベントも購読します。 このイベントは、ビデオの再生の準備が整い、Dispose オーバーライド内から登録が解除されたときに発生します。
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
protected override void Dispose(bool disposing)
{
if (disposing)
{
_videoView.Prepared -= OnVideoViewPrepared;
_videoView.Dispose();
_videoView = null;
_video = null;
}
base.Dispose(disposing);
}
...
}
Prepared イベントからの解除に加えて、Dispose オーバーライドは、ネイティブ ビューのクリーンアップも実行します。
注意
Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。
プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが実装されています。このボタンは Android の MediaController 型によって提供されます。
Video.AreTransportControlsEnabled プロパティが true に設定されている場合は、MediaController のメディア プレーヤーとして VideoView が設定されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に UpdateTransportControlsEnabled で MauiVideoPlayer メソッドが呼び出されるために発生します。
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
MediaController _mediaController;
Video _video;
...
public void UpdateTransportControlsEnabled()
{
if (_video.AreTransportControlsEnabled)
{
_mediaController = new MediaController(_context);
_mediaController.SetMediaPlayer(_videoView);
_videoView.SetMediaController(_mediaController);
}
else
{
_videoView.SetMediaController(null);
if (_mediaController != null)
{
_mediaController.SetMediaPlayer(null);
_mediaController = null;
}
}
}
...
}
トランスポートコントロールは使用されない場合、フェードアウトしますが、ビデオをタップすることで復元できます。
Video.AreTransportControlsEnabled プロパティが false に設定されている場合、MediaController は VideoView のメディア プレーヤーとして削除されます。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。
iOS と Mac Catalyst
ビデオは、iOS と Mac Catalyst で AVPlayer と AVPlayerViewController を使用して再生されます。 ただし、ここでは、これらの型は 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 は ViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は AVPlayer と AVPlayerViewController ネイティブ ビューをカプセル化する型を指定します。
CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。
ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。
DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、Dispose インスタンスで MauiVideoPlayer メソッドを呼び出します。
プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdatePosition();
}
...
}
各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。
プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、MauiVideoPlayer クラスで定義されたメソッドを呼び出します。
iOS および Mac Catalyst では、MauiVideoPlayer クラスは AVPlayer 型と AVPlayerViewController 型をカプセル化して、ネイティブ ビューをハンドラーから分離して保持します。
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
...
public MauiVideoPlayer(Video video)
{
_video = video;
_playerViewController = new AVPlayerViewController();
_player = new AVPlayer();
_playerViewController.Player = _player;
_playerViewController.View.Frame = this.Bounds;
#if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER
// On iOS 16 and Mac Catalyst 16, for Shell-based apps, the AVPlayerViewController has to be added to the parent ViewController, otherwise the transport controls won't be displayed.
var viewController = WindowStateManager.Default.GetCurrentUIViewController();
// If there's no view controller, assume it's not Shell and continue because the transport controls will still be displayed.
if (viewController?.View is not null)
{
// Zero out the safe area insets of the AVPlayerViewController
UIEdgeInsets insets = viewController.View.SafeAreaInsets;
_playerViewController.AdditionalSafeAreaInsets = new UIEdgeInsets(insets.Top * -1, insets.Left, insets.Bottom * -1, insets.Right);
// Add the View from the AVPlayerViewController to the parent ViewController
viewController.View.AddSubview(_playerViewController.View);
}
#endif
// Use the View from the AVPlayerViewController as the native control
AddSubview(_playerViewController.View);
}
...
}
}
MauiVideoPlayer から派生する UIView は、iOS および Mac Catalyst の基本クラスであり、コンテンツを表示し、そのコンテンツとのユーザー操作を処理するオブジェクトです。 このコンストラクターは、メディア ファイルの再生とタイミングを管理する AVPlayer オブジェクトを作成し、それを Player の AVPlayerViewController プロパティ値として設定します。
AVPlayerViewController は AVPlayer からのコンテンツを表示し、トランスポート コントロールとその他の機能を表示します。 コントロールのサイズと位置が設定されます。これにより、ビデオがページの中央に配置され、縦横比を維持しながら使用可能な領域を埋めるように拡張されます。 iOS 16 と Mac Catalyst 16 では、AVPlayerViewController をシェルベースのアプリの親 ViewController に追加する必要があります。それ以外の場合、トランスポート コントロールは表示されません。 その後、AVPlayerViewController からのネイティブ ビューがページに追加されます。
Dispose メソッドは、ネイティブ ビューのクリーンアップを実行します。
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
...
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
DestroyPlayedToEndObserver();
_player.ReplaceCurrentItemWithPlayerItem(null);
_player.Dispose();
}
if (_playerViewController != null)
_playerViewController.Dispose();
_video = null;
}
base.Dispose(disposing);
}
...
}
一部のシナリオでは、ビデオ再生ページから移動した後もビデオの再生が続行されます。 ビデオを停止するには、ReplaceCurrentItemWithPlayerItem は null オーバーライドで Dispose に設定し、その他のネイティブ ビュー クリーンアップを実行します。
注意
Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。
プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが含まれており、これらは AVPlayerViewController 型によって提供されます。
Video.AreTransportControlsEnabled プロパティが true に設定されている場合、AVPlayerViewController でその再生コントロールが表示されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に UpdateTransportControlsEnabled で MauiVideoPlayer メソッドが呼び出されるために発生します。
public class MauiVideoPlayer : UIView
{
AVPlayerViewController _playerViewController;
Video _video;
...
public void UpdateTransportControlsEnabled()
{
_playerViewController.ShowsPlaybackControls = _video.AreTransportControlsEnabled;
}
...
}
トランスポートコントロールは使用されない場合、フェードアウトしますが、ビデオをタップすることで復元できます。
Video.AreTransportControlsEnabled プロパティが false に設定されている場合、AVPlayerViewController でその再生コントロールは表示されません。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。
Windows
ビデオは、Windows で MediaPlayerElement で再生されます。 ただし、ここでは、ネイティブ ビューをハンドラーから分離し続けるために、MediaPlayerElement は MauiVideoPlayer 型にカプセル化されています。 次の例は、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);
}
...
}
}
VideoHandler は ViewHandler<TVirtualView,TPlatformView> クラスから派生し、ジェネリック Video 引数はクロスプラットフォーム コントロール型を指定し、MauiVideoPlayer 引数は MediaPlayerElement ネイティブ ビューをカプセル化する型を指定します。
CreatePlatformView オーバーライドは MauiVideoPlayer オブジェクトを作成して返します。
ConnectHandler オーバーライドは、必要なネイティブ ビューのセットアップを実行する場所です。
DisconnectHandler オーバーライドは、ネイティブ ビューのクリーンアップを実行する場所であり、Dispose インスタンスで MauiVideoPlayer メソッドを呼び出します。
プラットフォーム ハンドラーは、プロパティ マッパー ディクショナリで定義されている各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdatePosition();
}
...
}
各 Action は、クロスプラットフォーム コントロールでのプロパティの変更に応答して実行され、引数としてハンドラー インスタンスとクロスプラットフォーム コントロール インスタンスを必要とする static メソッドです。 いずれの場合も、Action は MauiVideoPlayer 型で定義されたメソッドを呼び出します。
プラットフォーム ハンドラーは、コマンド マッパー ディクショナリで定義された各 Action も実装する必要があります。
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
各 Action は、クロスプラットフォーム コントロールから送信されるコマンドに応答して実行されます。これは、ハンドラー インスタンスとクロスプラットフォームコントロールインスタンス、およびオプションのデータを引数として必要とする static メソッドです。 いずれの場合も、Action はオプションのデータを抽出した後、MauiVideoPlayer クラスで定義されたメソッドを呼び出します。
Windows では、ネイティブ ビューをハンドラーから分離させたままにするため、MauiVideoPlayer クラスは MediaPlayerElement をカプセル化します。
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public MauiVideoPlayer(Video video)
{
_video = video;
_mediaPlayerElement = new MediaPlayerElement();
this.Children.Add(_mediaPlayerElement);
}
...
}
}
MauiVideoPlayer は Grid から派生し、MediaPlayerElement は Grid の子として追加されます。 これにより、MediaPlayerElement は使用可能なすべての領域を埋めるように自動でサイズ調整されます。
Dispose メソッドは、ネイティブ ビューのクリーンアップを実行します。
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void Dispose()
{
if (_isMediaPlayerAttached)
{
_mediaPlayerElement.MediaPlayer.MediaOpened -= OnMediaPlayerMediaOpened;
_mediaPlayerElement.MediaPlayer.Dispose();
}
_mediaPlayerElement = null;
}
...
}
MediaOpened イベントからの解除に加えて、Dispose オーバーライドは、ネイティブ ビューのクリーンアップも実行します。
注意
Dispose オーバーライドは、ハンドラーの DisconnectHandler オーバーライドによって呼び出されます。
プラットフォーム トランスポート コントロールには、ビデオの再生、一時停止、停止を行うボタンが含まれており、これらは MediaPlayerElement 型によって提供されます。
Video.AreTransportControlsEnabled プロパティが true に設定されている場合、MediaPlayerElement でその再生コントロールが表示されます。 これは、AreTransportControlsEnabled プロパティが設定されると、ハンドラーのプロパティ マッパーによって MapAreTransportControlsEnabled メソッドが呼び出されることを確認し、次に UpdateTransportControlsEnabled で MauiVideoPlayer メソッドが呼び出されるために発生します。
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateTransportControlsEnabled()
{
_mediaPlayerElement.AreTransportControlsEnabled = _video.AreTransportControlsEnabled;
}
...
}
Video.AreTransportControlsEnabled プロパティが false に設定されている場合、MediaPlayerElement でその再生コントロールは表示されません。 このシナリオでは、ビデオ プレイバックをプログラムで制御したり、独自のトランスポート コントロールを提供したりできます。 詳細については、「カスタム トランスポート コントロールの作成」を参照してください。
クロスプラットフォーム コントロールをプラットフォーム コントロールに変換する
Element から派生した .NET MAUI クロスプラットフォーム コントロールは、ToPlatform 拡張メソッドを使用して、基になるプラットフォーム コントロールに変換できます。
- Android では、ToPlatform は .NET MAUI コントロールを Android View オブジェクトに変換します。
- iOS および Mac Catalyst では、ToPlatform は .NET MAUI コントロールを UIView オブジェクトに変換します。
- Windows では、ToPlatform は .NET MAUI コントロールを
FrameworkElementオブジェクトに変換します。
注意
ToPlatform 要素は、Microsoft.Maui.Platform 名前空間内にあります。
すべてのプラットフォームで、ToPlatform メソッドには MauiContext 引数が必要です。
ToPlatform メソッドは、クロスプラットフォーム コントロールを、プラットフォームの部分ハンドラー クラスなど、プラットフォーム コードから基になるプラットフォームのコントロールに変換できます。
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
// Convert cross-platform control to its underlying platform control
MauiVideoPlayer mvp = (MauiVideoPlayer)video.ToPlatform(handler.MauiContext);
...
}
...
}
}
この例では、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 です。
ネイティブイベントを購読する
ページまたはビューからクロスプラットフォーム コントロールを使用する場合は、ネイティブ プラットフォーム イベントを直接サブスクライブすることが必要になる場合があります。 ネイティブ ビューが使用可能になったときにサブスクライブするには HandlerChanged イベントを使用し、ハンドラーが置き換えられるか切断される前にサブスクライブを解除する HandlerChanging イベントを使用します。
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
myEntry.HandlerChanged += OnHandlerChanged;
myEntry.HandlerChanging += OnHandlerChanging;
}
void OnHandlerChanged(object? sender, EventArgs e)
{
#if WINDOWS
if (sender is Entry entry &&
entry.Handler?.PlatformView is Microsoft.UI.Xaml.Controls.TextBox textBox)
{
textBox.GotFocus += OnNativeGotFocus;
}
#endif
}
void OnHandlerChanging(object? sender, HandlerChangingEventArgs e)
{
#if WINDOWS
if (e.OldHandler?.PlatformView is Microsoft.UI.Xaml.Controls.TextBox textBox)
{
textBox.GotFocus -= OnNativeGotFocus;
}
#endif
}
#if WINDOWS
void OnNativeGotFocus(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
// Handle native focus gained
}
#endif
}
重要
HandlerChangingを使用して、e.OldHandlerのネイティブ イベントを常にサブスクライブ解除します。 サブスクライブを解除しないと、ネイティブ ビューはマネージド デリゲートへの参照を保持し続けるため、コントロールがガベージ コレクションされなくなります。 このリークはサイレントで累積的であり、すぐにクラッシュすることはありませんが、OS によってアプリが終了するまで、リストの負荷の高い UI ( CollectionView 内のコントロールや ListViewDataTemplateなど) でメモリが着実に増加します。
HandlerChanging イベントは、ハンドラーが置き換えられるか切断されようとするたびに、OldHandlerに設定された古いハンドラーで発生します。 この時点で、ネイティブ ビュー (PlatformView) に引き続きアクセスできるため、ネイティブ イベント サブスクリプションを削除するための適切で安全な場所になります。
ビデオを再生する
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 クラスには、TypeConverter を参照する VideoSourceConverter 属性が含まれています。
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class VideoSourceConverter : TypeConverter, IExtendedTypeConverter
{
object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
{
if (!string.IsNullOrWhiteSpace(value))
{
Uri uri;
return Uri.TryCreate(value, UriKind.Absolute, out uri) && uri.Scheme != "file" ?
VideoSource.FromUri(value) : VideoSource.FromResource(value);
}
throw new InvalidOperationException("Cannot convert null or whitespace to VideoSource.");
}
}
}
この型コンバーターは、XAML の文字列に Source プロパティが設定されると起動します。
ConvertFromInvariantString メソッドが文字列を Uri オブジェクトに変換しようとします。 これが成功し、スキームが file でない場合は、このメソッドは UriVideoSource を返します。 それ以外の場合は ResourceVideoSource を返します。
Web ビデオの再生
UriVideoSource クラスを使用すると、リモートのビデオを URI で指定できます。 これにより、Uri 型の string プロパティを定義します。
namespace VideoDemos.Controls
{
public class UriVideoSource : VideoSource
{
public static readonly BindableProperty UriProperty =
BindableProperty.Create(nameof(Uri), typeof(string), typeof(UriVideoSource));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
}
Source プロパティが UriVideoSourceに設定されている場合、ハンドラーのプロパティ マッパーによって、MapSource メソッドが確実に呼び出されます。
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource メソッドは、ハンドラーの UpdateSource プロパティの PlatformView メソッドを呼び出します。
PlatformView 型の MauiVideoPlayer プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。
Android
ビデオは、Android で VideoView を使用して再生されます。 次のコード例は、UpdateSource メソッドが Source 型の場合に UriVideoSource プロパティを処理する方法を示しています。
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
{
_videoView.SetVideoURI(Uri.Parse(uri));
hasSetSource = true;
}
}
...
if (hasSetSource && _video.AutoPlay)
{
_videoView.Start();
}
}
...
}
}
UriVideoSource 型のオブジェクトを処理する場合は、SetVideoUri の VideoView メソッド を使用して再生するビデオを指定し、URI 文字列から Android の Uri オブジェクトを作成します。
AutoPlay プロパティには VideoView に相当するものがないため、新しいビデオが設定されると Start メソッドが呼び出されます。
iOS と Mac Catalyst
iOS と Mac Catalyst でビデオを再生するには、AVAsset 型のオブジェクトを作成してビデオがカプセル化され、それが AVPlayerItem の作成に使用されて AVPlayer オブジェクトに渡されます。 次のコード例は、UpdateSource メソッドが Source 型の場合に UriVideoSource プロパティを処理する方法を示しています。
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerItem _playerItem;
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
asset = AVAsset.FromUrl(new NSUrl(uri));
}
...
if (asset != null)
_playerItem = new AVPlayerItem(asset);
else
_playerItem = null;
_player.ReplaceCurrentItemWithPlayerItem(_playerItem);
if (_playerItem != null && _video.AutoPlay)
{
_player.Play();
}
}
...
}
}
UriVideoSource 型のオブジェクトを処理する場合、静的な AVAsset.FromUrl メソッドを使用して再生するビデオを指定し、URI 文字列から iOS NSUrl オブジェクトを作成します。
AutoPlay プロパティには iOS ビデオ クラスに相当するものがないため、このプロパティは UpdateSource メソッドの最後に検査され、Play オブジェクトの AVPlayer メソッドが呼び出されます。
iOS では、ビデオ再生ページから移動した後もビデオが再生され続ける場合があります。 ビデオを停止するには、ReplaceCurrentItemWithPlayerItem オーバーライドで null を Dispose に設定します。
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
_player.ReplaceCurrentItemWithPlayerItem(null);
...
}
...
}
base.Dispose(disposing);
}
Windows
Windows では、ビデオは MediaPlayerElement で再生されます。 次のコード例は、UpdateSource メソッドが Source 型の場合に UriVideoSource プロパティを処理する方法を示しています。
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public async void UpdateSource()
{
bool hasSetSource = false;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
{
_mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(uri));
hasSetSource = true;
}
}
...
if (hasSetSource && !_isMediaPlayerAttached)
{
_isMediaPlayerAttached = true;
_mediaPlayerElement.MediaPlayer.MediaOpened += OnMediaPlayerMediaOpened;
}
if (hasSetSource && _video.AutoPlay)
{
_mediaPlayerElement.AutoPlay = true;
}
}
...
}
}
UriVideoSource 型のオブジェクトを処理する場合、MediaPlayerElement.Source プロパティには、再生するビデオの URI を使用して MediaSource を初期化する Uri オブジェクトが設定されます。
MediaPlayerElement.Source が設定されると、OnMediaPlayerMediaOpened イベントに対して MediaPlayerElement.MediaPlayer.MediaOpened イベント ハンドラー メソッドが登録されます。 このイベント ハンドラーは、Duration コントロールの Video プロパティを設定するために使用されます。
UpdateSource メソッドの最後に Video.AutoPlay プロパティが調べられ、true の場合は、ビデオ再生を開始するように MediaPlayerElement.AutoPlay プロパティが true に設定されます。
ビデオ リソースの再生
ResourceVideoSource クラスは、アプリに埋め込まれているビデオ ファイルにアクセスするために使用されます。 これにより、Path 型の string プロパティを定義します。
namespace VideoDemos.Controls
{
public class ResourceVideoSource : VideoSource
{
public static readonly BindableProperty PathProperty =
BindableProperty.Create(nameof(Path), typeof(string), typeof(ResourceVideoSource));
public string Path
{
get { return (string)GetValue(PathProperty); }
set { SetValue(PathProperty, value); }
}
}
}
Source プロパティが ResourceVideoSourceに設定されている場合、ハンドラーのプロパティ マッパーによって、MapSource メソッドが確実に呼び出されます。
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource メソッドは、ハンドラーの UpdateSource プロパティの PlatformView メソッドを呼び出します。
PlatformView 型の MauiVideoPlayer プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。
Android
次のコード例は、UpdateSource メソッドが Source 型の場合に ResourceVideoSource プロパティを処理する方法を示しています。
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Context _context;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
...
else if (_video.Source is ResourceVideoSource)
{
string package = Context.PackageName;
string path = (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
string assetFilePath = "content://" + package + "/" + path;
_videoView.SetVideoPath(assetFilePath);
hasSetSource = true;
}
}
...
}
...
}
}
ResourceVideoSource 型のオブジェクトを処理する場合、SetVideoPath の VideoView メソッドを使用して再生するビデオを指定し、アプリのパッケージ名とビデオのファイル名を組み合わせた文字列引数を指定します。
リソース ビデオ ファイルはパッケージの アセット フォルダーに格納され、コンテンツ プロバイダーがアクセスする必要があります。 コンテンツ プロバイダーは VideoProvider クラスによって提供され、ビデオ ファイルへのアクセスを提供する AssetFileDescriptor オブジェクトを作成します。
using Android.Content;
using Android.Content.Res;
using Android.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
[ContentProvider(new string[] { "com.companyname.videodemos" })]
public class VideoProvider : ContentProvider
{
public override AssetFileDescriptor OpenAssetFile(Uri uri, string mode)
{
var assets = Context.Assets;
string fileName = uri.LastPathSegment;
if (fileName == null)
throw new FileNotFoundException();
AssetFileDescriptor afd = null;
try
{
afd = assets.OpenFd(fileName);
}
catch (IOException ex)
{
Debug.WriteLine(ex);
}
return afd;
}
public override bool OnCreate()
{
return false;
}
...
}
}
iOS と Mac Catalyst
次のコード例は、UpdateSource メソッドが Source 型の場合に ResourceVideoSource プロパティを処理する方法を示しています。
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
...
else if (_video.Source is ResourceVideoSource)
{
string path = (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
string directory = Path.GetDirectoryName(path);
string filename = Path.GetFileNameWithoutExtension(path);
string extension = Path.GetExtension(path).Substring(1);
NSUrl url = NSBundle.MainBundle.GetUrlForResource(filename, extension, directory);
asset = AVAsset.FromUrl(url);
}
}
...
}
...
}
}
ResourceVideoSource 型のオブジェクトを処理する場合、GetUrlForResource の NSBundle メソッドを使用してアプリ パッケージからファイルを取得します。 完全なパスをファイル名、拡張子、ディレクトリに分割する必要があります。
iOS では、ビデオ再生ページから移動した後もビデオが再生され続ける場合があります。 ビデオを停止するには、ReplaceCurrentItemWithPlayerItem オーバーライドで null を Dispose に設定します。
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
_player.ReplaceCurrentItemWithPlayerItem(null);
...
}
...
}
base.Dispose(disposing);
}
Windows
次のコード例は、UpdateSource メソッドが Source 型の場合に ResourceVideoSource プロパティを処理する方法を示しています。
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public async void UpdateSource()
{
bool hasSetSource = false;
...
else if (_video.Source is ResourceVideoSource)
{
string path = "ms-appx:///" + (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
_mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(path));
hasSetSource = true;
}
}
...
}
...
}
}
ResourceVideoSource 型のオブジェクトを処理する場合、MediaPlayerElement.Source プロパティは、MediaSource というプレフィックスが付いたビデオ リソースのパスで Uri を初期化する ms-appx:/// オブジェクトに設定されます。
デバイスのライブラリからビデオ ファイルを再生する
FileVideoSource クラスは、デバイスのビデオ ライブラリからビデオ ファイルにアクセスするために使用します。 これにより、File 型の string プロパティを定義します。
namespace VideoDemos.Controls
{
public class FileVideoSource : VideoSource
{
public static readonly BindableProperty FileProperty =
BindableProperty.Create(nameof(File), typeof(string), typeof(FileVideoSource));
public string File
{
get { return (string)GetValue(FileProperty); }
set { SetValue(FileProperty, value); }
}
}
}
Source プロパティが FileVideoSourceに設定されている場合、ハンドラーのプロパティ マッパーによって、MapSource メソッドが確実に呼び出されます。
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource メソッドは、ハンドラーの UpdateSource プロパティの PlatformView メソッドを呼び出します。
PlatformView 型の MauiVideoPlayer プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。
Android
次のコード例は、UpdateSource メソッドが Source 型の場合に FileVideoSource プロパティを処理する方法を示しています。
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
...
else if (_video.Source is FileVideoSource)
{
string filename = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(filename))
{
_videoView.SetVideoPath(filename);
hasSetSource = true;
}
}
...
}
...
}
}
FileVideoSource型のオブジェクトを処理する場合は、SetVideoPathのVideoViewメソッドで再生するビデオ ファイルを特定します。
iOS と Mac Catalyst
次のコード例は、UpdateSource メソッドが Source 型の場合に FileVideoSource プロパティを処理する方法を示しています。
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
...
else if (_video.Source is FileVideoSource)
{
string uri = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(uri))
asset = AVAsset.FromUrl(NSUrl.CreateFileUrl(new [] { uri }));
}
...
}
...
}
}
FileVideoSource 型のオブジェクトを処理する場合、再生するビデオ ファイルが静的な AVAsset.FromUrl メソッドを使用して指定され、NSUrl.CreateFileUrl メソッドを使用して文字列 URI から iOS NSUrl オブジェクトが作成されます。
Windows
次のコード例は、UpdateSource メソッドが Source 型の場合に FileVideoSource プロパティを処理する方法を示しています。
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public async void UpdateSource()
{
bool hasSetSource = false;
...
else if (_video.Source is FileVideoSource)
{
string filename = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(filename))
{
StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename);
_mediaPlayerElement.Source = MediaSource.CreateFromStorageFile(storageFile);
hasSetSource = true;
}
}
...
}
...
}
}
FileVideoSource 型のオブジェクトを処理する場合、ビデオ ファイル名は StorageFile オブジェクトに変換されます。 次に、MediaSource.CreateFromStorageFile メソッドは、MediaSource プロパティの値として設定される MediaPlayerElement.Source オブジェクトを返します。
ビデオをループ再生する
Video クラスは IsLooping プロパティを定義します。これにより、コントロールはビデオの位置が末尾に到達すると、自動的に開始位置に設定します。 既定では false であり、ビデオが自動的にループしないことを示します。
IsLooping プロパティが設定されると、ハンドラーのプロパティ マッパーによって、MapIsLooping メソッドが確実に呼び出されます。
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
MapIsLooping メソッドは、ハンドラーの UpdateIsLooping プロパティの PlatformView メソッドを呼び出します。
PlatformView 型の MauiVideoPlayer プロパティは、各プラットフォームでのビデオ プレーヤーの実装をネイティブ ビューで提供します。
Android
次のコード例は、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 メソッドは、MauiVideoPlayer コールバックを提供するオブジェクトとして OnPrepared を設定します。 このコールバックは、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 オーバーライドは、DestroyPlayedToEndObserver 通知のオブザーバーを削除する AVPlayerItem.DidPlayToEndTimeNotification メソッドを呼び出し、Dispose で NSObject メソッドを呼び出します。
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 クラスは、対応するイベントを引き起こす Play、Pause、Stop という名前のメソッドを定義し、コマンドを VideoHandler に送信します。
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public event EventHandler<VideoPositionEventArgs> PlayRequested;
public event EventHandler<VideoPositionEventArgs> PauseRequested;
public event EventHandler<VideoPositionEventArgs> StopRequested;
public void Play()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PlayRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PlayRequested), args);
}
public void Pause()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PauseRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PauseRequested), args);
}
public void Stop()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
StopRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.StopRequested), args);
}
}
}
VideoPositionEventArgs このクラスは、コンストラクターをで設定できる Position プロパティを定義します。 このプロパティは、ビデオの再生を開始、一時停止、停止した位置を表します。
Play、Pause、Stop メソッドの最後の行は、コマンドと VideoHandler に関連付けられたデータを送信します。
CommandMapper の VideoHandler は、コマンドを受信したときに実行するアクションにコマンド名をマップします。 たとえば、VideoHandler が PlayRequested コマンドを受け取ると、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 つの各メソッドは、コマンドで送信されたデータを使用して、ビデオが再生、一時停止、または停止された位置をログに記録します。
このメカニズムにより、Play コントロールでPause、StopまたはVideo メソッドが呼び出されると、そのネイティブ ビューでビデオの再生、一時停止、または停止が指示され、ビデオが再生、一時停止、または停止された位置がログに記録されます。 これはすべて分離されたアプローチを使用して行われます。ネイティブ ビューでクロスプラットフォーム イベントをサブスクライブする必要はありません。
ビデオの状態
再生、一時停止、停止などの機能を実装するだけでは、トランスポート コントロールをサポートするためには不十分です。 多くの場合、再生と一時停止の機能は同じボタンで実装され、動画が現在再生中か一時停止中かを示すために外観が変化します。 さらに、ビデオがまだ読み込まれていない場合、ボタンは有効にすべきではありません。
これらの要件は、ビデオ プレーヤーの現在の状態、つまり再生中か一時停止中か、またはまだ再生準備ができていないかということが示される必要があるということを、意味します。 この状態は、列挙型で表すことができます:
public enum VideoStatus
{
NotReady,
Playing,
Paused
}
Video クラスは、Status という名前で VideoStatus 型の、読み取り専用のバインド可能なプロパティが定義されます。 このプロパティは、コントロールのハンドラーからのみ設定されるべきであるため、読み取り専用として定義されています:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey StatusPropertyKey =
BindableProperty.CreateReadOnly(nameof(Status), typeof(VideoStatus), typeof(Video), VideoStatus.NotReady);
public static readonly BindableProperty StatusProperty = StatusPropertyKey.BindableProperty;
public VideoStatus Status
{
get { return (VideoStatus)GetValue(StatusProperty); }
}
VideoStatus IVideoController.Status
{
get { return Status; }
set { SetValue(StatusPropertyKey, value); }
}
...
}
}
通常、読み取り専用のバインド可能なプロパティには、クラス内からの設定を可能にするため、set プロパティに対するプライベート Status アクセサーが存在します。 ただし、ハンドラーによってサポートされる View の派生物の場合、プロパティはクラスの外部、ただしコントロールのハンドラーによってのみ設定する必要があります。
このため、IVideoController.Status という名前の別のプロパティが定義されます。 これは、明示的なインターフェイスの実装であり、IVideoController クラスによって実装された Video インターフェイスによって可能となります。
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
このインターフェイスにより、Video の外部のクラスが Status インターフェイスを参照してIVideoController プロパティを設定することが可能になります。 このプロパティは他のクラスやハンドラーからも設定できますが、誤って設定されることはほとんどありません。 最も重要なこととして、Status プロパティはデータ バインドを通じて設定することはできません。
Status プロパティの更新を維持するためにハンドラーの実装を支援するために、Video クラスは UpdateStatus イベントとコマンドを定義します。
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public event EventHandler UpdateStatus;
IDispatcherTimer _timer;
public Video()
{
_timer = Dispatcher.CreateTimer();
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += OnTimerTick;
_timer.Start();
}
~Video() => _timer.Tick -= OnTimerTick;
void OnTimerTick(object sender, EventArgs e)
{
UpdateStatus?.Invoke(this, EventArgs.Empty);
Handler?.Invoke(nameof(Video.UpdateStatus));
}
...
}
}
OnTimerTick イベント ハンドラーは 10 分の 1 秒ごとに実行され、UpdateStatus イベントを発生して UpdateStatus コマンドを呼び出します。
UpdateStatus コマンドがVideo コントロールから ハンドラーに送信されると、ハンドラーのコマンド マッパーによってMapUpdateStatus メソッドが確実に 呼び出されます。
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
MapUpdateStatus メソッドは、ハンドラーの UpdateStatus プロパティの PlatformView メソッドを呼び出します。
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 プロパティを使い、Status オブジェクトの Video プロパティを 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;
...
}
...
}
}
AVPlayer プロパティを設定するには、Status の 2つのプロパティ (Status 型の AVPlayerStatus プロパティと TimeControlStatus 型の AVPlayerTimeControlStatus プロパティ) にアクセスする必要があります。 次に、Status にキャストすることで、Video オブジェクトに IVideoController プロパティを設定することができます。
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 プロパティの値を決定します。 次に、Status にキャストすることで、Video オブジェクトに IVideoController プロパティを設定することができます。
位置バー
各プラットフォームで実装されているトランスポート コントロールには、位置バーが含まれます。 このバーはスライダーまたはスクロール バーに似ており、ビデオの合計時間内の現在の位置を示します。 ユーザーは位置バーを操作して、ビデオ内の新しい位置へと前後に移動することができます。
独自の位置バーを実装するには、Video クラスがビデオの再生時間と、その再生時間内における現在の位置を知っている必要があります。
継続時間
Video コントロールがカスタムの位置バーをサポートするために必要な情報の 1 つに、ビデオの再生時間があります。
Video クラスにより、Duration という名前で TimeSpan 型、読み取り専用のバインド可能なプロパティが定義されます。 このプロパティは、コントロールのハンドラーからのみ設定されるべきであるため、読み取り専用として定義されています:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey DurationPropertyKey =
BindableProperty.CreateReadOnly(nameof(Duration), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public static readonly BindableProperty DurationProperty = DurationPropertyKey.BindableProperty;
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
}
TimeSpan IVideoController.Duration
{
get { return Duration; }
set { SetValue(DurationPropertyKey, value); }
}
...
}
}
通常、読み取り専用のバインド可能なプロパティには、クラス内からの設定を可能にするため、set プロパティに対するプライベート Duration アクセサーが存在します。 ただし、ハンドラーによってサポートされる View の派生物の場合、プロパティはクラスの外部、ただしコントロールのハンドラーによってのみ設定する必要があります。
このため、IVideoController.Duration という名前の別のプロパティが定義されます。 これは、明示的なインターフェイスの実装であり、IVideoController クラスによって実装された Video インターフェイスによって可能となります。
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
このインターフェイスにより、Video の外部のクラスが Duration インターフェイスを参照してIVideoController プロパティを設定することが可能になります。 このプロパティは他のクラスやハンドラーからも設定できますが、誤って設定されることはほとんどありません。 最も重要なこととして、Duration プロパティはデータ バインドを通じて設定することはできません。
Source コントロールの Video プロパティが設定された直後は、ビデオの再生時間は表示されません。 ネイティブ ビューが再生時間を決定するには、その前に、部分的にビデオをダウンロードする必要があります。
Android
Androidでは、VideoView.Duration プロパティは、VideoView.Prepared イベントが発生した後の有効期間をミリ秒単位で報告します。
MauiVideoPlayer クラスは Prepared イベントハンドラーを使って Duration プロパティの値を取得します:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
void OnVideoViewPrepared(object sender, EventArgs args)
{
...
((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
}
...
}
}
iOS と Mac Catalyst
iOS および Mac Catalyst では、ビデオの再生時間は AVPlayerItem.Duration プロパティから取得されますが、AVPlayerItem が作成された直後に行われるわけではありません。 iOS のオブザーバーを Duration プロパティに設定することはできますが、MauiVideoPlayer クラスは 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 メソッドを呼び出し、メインスレッド上で Duration オブジェクトの Video プロパティを IVideoController にキャストして設定します。 これは、MediaPlayerElement.MediaPlayer.MediaOpened イベントがバックグラウンド スレッドで処理されるために必要です。 メイン スレッドでコードを実行する方法の詳細については、「.NET MAUI UI スレッドでスレッドを作成する」をご覧ください。
ポジション
また、Video コントロールには Position プロパティが必要で、ビデオの再生に合わせてゼロから Duration まで増加します。
Video クラスは、パブリック get と set アクセサーを持つバインド可能なプロパティとしてこのプロパティを実装します。
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public static readonly BindableProperty PositionProperty =
BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public TimeSpan Position
{
get { return (TimeSpan)GetValue(PositionProperty); }
set { SetValue(PositionProperty, value); }
}
...
}
}
get アクセサーは、再生中のビデオの現在位置を返します。
set アクセサーは、ユーザーが位置バーを操作すると、ビデオの位置を前方または後方に動かして反応します。
Android、iOS、Mac Catalyst では、現在の位置を取得するプロパティには get アクセサーのみが含まれます。 代わりに、位置を設定する Seek メソッドを使用できます。 これは、固有の問題がある単一の Position プロパティを使用するよりも、より賢明なアプローチです。 動画が再生されると、Position プロパティは新しい位置を反映するために継続的に更新される必要があります。 しかし、Position プロパティのほとんどの変更が、ビデオプレーヤーをビデオの新しい位置に移動させる原因になることは望ましくありません。 このような処理の結果、ビデオ プレーヤーは Position プロパティの最後の値までシークする処理で応答するので、ビデオは進みません。
Position プロパティを get アクセサーと set アクセサーで実装することの難しさにもかかわらず、この方法が使われるのは、データ バインディングを利用できるからです。
Position コントロールの Video プロパティは、位置の表示と新しい位置の検索の両方に使用される Slider にバインドできます。 ただし、Position プロパティを実装する際には、フィードバック ループを避けるためにいくつかの事前の注意が必要です。
Android
Android では、VideoView.CurrentPosition プロパティがビデオの現在位置を示します。
MauiVideoPlayer クラスは、Position プロパティを設定すると同時に、UpdateStatus メソッドで Duration プロパティを設定します。
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
public void UpdateStatus()
{
...
TimeSpan timeSpan = TimeSpan.FromMilliseconds(_videoView.CurrentPosition);
_video.Position = timeSpan;
}
public void UpdatePosition()
{
if (Math.Abs(_videoView.CurrentPosition - _video.Position.TotalMilliseconds) > 1000)
{
_videoView.SeekTo((int)_video.Position.TotalMilliseconds);
}
}
...
}
}
Position プロパティが UpdateStatus メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。
UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 それ以外の場合は、ビデオの位置を変更するたびに、ビデオが到達したのと同じ位置に移動されます。 このフィードバック ループを避けるため、UpdatePosition は、Seek プロパティと VideoView の現在位置との差が 1 秒を超えたときのみ、Position オブジェクトの VideoView メソッドを呼び出します。
iOS と Mac Catalyst
iOS と Mac Catalyst では、AVPlayerItem.CurrentTime プロパティはビデオの現在の位置を示します。
MauiVideoPlayer クラスは、Position プロパティを設定すると同時に、UpdateStatus メソッドで Duration プロパティを設定します。
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerItem _playerItem;
Video _video;
...
TimeSpan ConvertTime(CMTime cmTime)
{
return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
}
public void UpdateStatus()
{
...
if (_playerItem != null)
{
...
_video.Position = ConvertTime(_playerItem.CurrentTime);
}
}
public void UpdatePosition()
{
TimeSpan controlPosition = ConvertTime(_player.CurrentTime);
if (Math.Abs((controlPosition - _video.Position).TotalSeconds) > 1)
{
_player.Seek(CMTime.FromSeconds(_video.Position.TotalSeconds, 1));
}
}
...
}
}
Position プロパティが UpdateStatus メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。
UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 それ以外の場合は、ビデオの位置を変更するたびに、ビデオが到達したのと同じ位置に移動されます。 このフィードバック ループを避けるため、UpdatePosition は、Seek プロパティと AVPlayer の現在位置との差が 1 秒を超えたときのみ、Position オブジェクトの AVPlayer メソッドを呼び出します。
Windows
Windows では、MediaPlayerElement.MediaPlayer.Position プロパティはビデオの現在の位置を示します。
MauiVideoPlayer クラスは、Position プロパティを設定すると同時に、UpdateStatus メソッドで Duration プロパティを設定します。
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateStatus()
{
if (_isMediaPlayerAttached)
{
...
_video.Position = _mediaPlayerElement.MediaPlayer.Position;
}
}
public void UpdatePosition()
{
if (_isMediaPlayerAttached)
{
if (Math.Abs((_mediaPlayerElement.MediaPlayer.Position - _video.Position).TotalSeconds) > 1)
{
_mediaPlayerElement.MediaPlayer.Position = _video.Position;
}
}
}
...
}
}
Position プロパティが UpdateStatus メソッドによって設定されるたびに、Position プロパティは PropertyChanged イベントを発生させ、ハンドラーのプロパティ マッパーが UpdatePosition メソッドを呼び出します。
UpdatePosition メソッドでは、ほとんどのプロパティの変更に対して何も行う必要はありません。 そうでなければ、ビデオの位置が変更されるたびに、到達した同じ位置に移動してしまいます。 このフィードバック ループを避けるため、UpdatePosition は、MediaPlayerElement.MediaPlayer.Position プロパティと Position の現在位置との差が 1 秒を超えたときのみ、MediaPlayerElement プロパティを設定します。
終了までの時間の計算
ビデオ プレーヤーにビデオの残り時間が表示されることがあります。 この値は、ビデオの開始時にはビデオの再生時間から始まり、ビデオの終了時には 0 まで減少します。
Video クラスには読み取り専用の TimeToEnd プロパティがあり、Duration プロパティと Position プロパティの変更に基づいて計算されます。
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey TimeToEndPropertyKey =
BindableProperty.CreateReadOnly(nameof(TimeToEnd), typeof(TimeSpan), typeof(Video), new TimeSpan());
public static readonly BindableProperty TimeToEndProperty = TimeToEndPropertyKey.BindableProperty;
public TimeSpan TimeToEnd
{
get { return (TimeSpan)GetValue(TimeToEndProperty); }
private set { SetValue(TimeToEndPropertyKey, value); }
}
void SetTimeToEnd()
{
TimeToEnd = Duration - Position;
}
...
}
}
SetTimeToEnd メソッドは、Duration プロパティと Position プロパティのプロパティ変更イベント ハンドラーから呼び出されます。
カスタム位置バー
カスタム位置バーは、Slider 型の Duration プロパティと Position プロパティを含む TimeSpan から派生したクラスを作成することで実装できます。
namespace VideoDemos.Controls
{
public class PositionSlider : Slider
{
public static readonly BindableProperty DurationProperty =
BindableProperty.Create(nameof(Duration), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(1),
propertyChanged: (bindable, oldValue, newValue) =>
{
double seconds = ((TimeSpan)newValue).TotalSeconds;
((Slider)bindable).Maximum = seconds <= 0 ? 1 : seconds;
});
public static readonly BindableProperty PositionProperty =
BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(0),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
double seconds = ((TimeSpan)newValue).TotalSeconds;
((Slider)bindable).Value = seconds;
});
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
public TimeSpan Position
{
get { return (TimeSpan)GetValue(PositionProperty); }
set { SetValue (PositionProperty, value); }
}
public PositionSlider()
{
PropertyChanged += (sender, args) =>
{
if (args.PropertyName == "Value")
{
TimeSpan newPosition = TimeSpan.FromSeconds(Value);
if (Math.Abs(newPosition.TotalSeconds - Position.TotalSeconds) / Duration.TotalSeconds > 0.01)
Position = newPosition;
}
};
}
}
}
Duration プロパティのプロパティ変更イベント ハンドラーは、Maximum の Slider プロパティを TotalSeconds 値の TimeSpan プロパティに設定します。 同様に、Position プロパティのプロパティ変更イベント ハンドラーは、Value の Slider プロパティを設定します。 これは、Slider の位置を PositionSlider が追跡するメカニズムです。
PositionSlider が基盤となる Slider から更新されるのは、ユーザーが Slider を操作してビデオを新しい位置に進めたり戻したりするというシナリオのみです。 これは、PropertyChanged コンストラクターの PositionSlider ハンドラーで検出されます。 このイベント ハンドラーでは、Value プロパティの変更をチェックし、それが Position プロパティと異なる場合は、Position プロパティから Value プロパティが設定されます。
ハンドラーの登録
カスタム コントロールとそのハンドラーは、アプリを使用する前に登録する必要があります。 これは、アプリ プロジェクトの CreateMauiApp クラス内の MauiProgram メソッド (アプリのクロスプラットフォーム エントリ ポイント) で発生する必要があります。
using VideoDemos.Controls;
using VideoDemos.Handlers;
namespace VideoDemos;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(Video), typeof(VideoHandler));
});
return builder.Build();
}
}
ハンドラは ConfigureMauiHandlers と AddHandler メソッドで登録されます。 AddHandler メソッドの最初の引数は、クロスプラットフォーム コントロール型で、2 番目の引数はそのハンドラー型です。
クロスプラットフォーム コントロールを利用する
ハンドラーをアプリに登録した後、クロスプラットフォーム コントロールを使用できます。
Web ビデオの再生
次の例に示すように、 Video コントロールはURL からビデオを再生できます:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayWebVideoPage"
Unloaded="OnContentPageUnloaded"
Title="Play web video">
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>
この例では、VideoSourceConverter クラスがURIを表す文字列を UriVideoSource に変換しています。 十分な量のデータがダウンロードされ、バッファリングされると、ビデオは読み込みと再生を開始します。 各プラットフォームで、トランスポート コントロールが使用されていない場合はフェード アウトしますが、ビデオをタップすることで表示を復元することができます。
ビデオ リソースの再生
アプリの ResourcesRaw フォルダーに MauiAsset ビルド アクションで埋め込まれたビデオ ファイルは、Video コントロールで再生できます:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayVideoResourcePage"
Unloaded="OnContentPageUnloaded"
Title="Play video resource">
<controls:Video x:Name="video"
Source="video.mp4" />
</ContentPage>
この例では、VideoSourceConverter クラスがビデオのファイル名を表す文字列を ResourceVideoSource に変換しています。 各プラットフォームとも、アプリのパッケージ内にあるファイルをダウンロードする必要がないため、ビデオソースを設定するとほぼ即座にビデオの再生が開始されます。 各プラットフォームで、トランスポート コントロールが使用されていない場合はフェード アウトしますが、ビデオをタップすることで表示を復元することができます。
デバイスのライブラリからビデオ ファイルを再生する
デバイスに保存されているビデオ ファイルを取得し、Video コントロールで再生できます。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayLibraryVideoPage"
Unloaded="OnContentPageUnloaded"
Title="Play library video">
<Grid RowDefinitions="*,Auto">
<controls:Video x:Name="video" />
<Button Grid.Row="1"
Text="Show Video Library"
Margin="10"
HorizontalOptions="Center"
Clicked="OnShowVideoLibraryClicked" />
</Grid>
</ContentPage>
Button がタップされると、その Clicked イベントハンドラーが実行されます。これを次のコード例に示します。
async void OnShowVideoLibraryClicked(object sender, EventArgs e)
{
Button button = sender as Button;
button.IsEnabled = false;
var pickedVideo = await MediaPicker.PickVideoAsync();
if (!string.IsNullOrWhiteSpace(pickedVideo?.FileName))
{
video.Source = new FileVideoSource
{
File = pickedVideo.FullPath
};
}
button.IsEnabled = true;
}
Clicked イベント ハンドラーで .NET MAUI の MediaPicker クラスを使用することにより、ユーザーはデバイスからビデオファイルを選択できます。 選択したビデオ ファイルは、FileVideoSource オブジェクトとしてカプセル化され、Source コントロールの Video プロパティとして設定されます。
MediaPicker クラスの詳細については、「メディア ピッカー」を参照してください。 各プラットフォームでは、ファイルがデバイス上にあり、ダウンロードする必要がないため、ビデオ ソースが設定されたすぐ後に、ビデオの再生が開始されます。 各プラットフォームで、トランスポート コントロールが使用されていない場合はフェード アウトしますが、ビデオをタップすることで表示を復元することができます。
ビデオ コントロールの構成
AutoPlay プロパティを false に設定すると、ビデオが自動的に開始することを防ぐことができます。
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
AutoPlay="False" />
AreTransportControlsEnabled プロパティを false に設定すると、トランスポート コントロールを抑止できます。
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
AreTransportControlsEnabled="False" />
AutoPlay とAreTransportControlsEnabled をfalseに設定すると、ビデオの再生が開始されず、再生を開始する方法がなくなります。 このシナリオでは、分離コード ファイルから Play メソッドを呼び出すか、独自のトランスポート コントロールを作成する必要があります。
さらに、IsLooping プロパティをtrue:に設定することで、ビデオをループさせることができます。
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
IsLooping="true" />
IsLooping プロパティを true に設定すると、Video コントロールがビデオの末尾に達した後、ビデオ位置を自動的に開始位置に設定するようになります。
カスタム トランスポート コントロールの使用
次の XAML の例は、ビデオの再生、一時停止、停止を行うカスタム トランスポート コントロールを示しています。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.CustomTransportPage"
Unloaded="OnContentPageUnloaded"
Title="Custom transport controls">
<Grid x:DataType="controls:Video"
RowDefinitions="*,Auto">
<controls:Video x:Name="video"
AutoPlay="False"
AreTransportControlsEnabled="False"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
<ActivityIndicator Color="Gray"
IsVisible="False">
<ActivityIndicator.Triggers>
<DataTrigger TargetType="ActivityIndicator"
Binding="{Binding Source={x:Reference video},
Path=Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsVisible"
Value="True" />
<Setter Property="IsRunning"
Value="True" />
</DataTrigger>
</ActivityIndicator.Triggers>
</ActivityIndicator>
<Grid Grid.Row="1"
Margin="0,10"
ColumnDefinitions="0.5*,0.5*"
BindingContext="{x:Reference video}">
<Button Text="▶️ Play"
HorizontalOptions="Center"
Clicked="OnPlayPauseButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.Playing}">
<Setter Property="Text"
Value="⏸ 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="⏹ Stop"
HorizontalOptions="Center"
Clicked="OnStopButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsEnabled"
Value="False" />
</DataTrigger>
</Button.Triggers>
</Button>
</Grid>
</Grid>
</ContentPage>
この例では、Video コントロールは AreTransportControlsEnabled プロパティを false に設定し、ビデオの再生と一時停止を行う Button と、ビデオの再生を停止する Button を定義しています。 ボタンの外観は、アイコンとテキストで構成されるボタンを作成するために、Unicode 文字とそのテキストに相当するものを使用して定義されます。
ビデオが再生されると、再生ボタンが一時停止ボタンに更新されます:
UI には、ビデオの読み込み中に表示される ActivityIndicator もも含まれます。 データ トリガーは、ActivityIndicator とボタンの有効化と無効化、および最初のボタンの再生と一時停止間の切り替えに使用されます。 データ トリガーの詳細については、「 データ トリガー」を参照してください。
分離コード ファイルでは、ボタンの Clicked イベントに対するイベント ハンドラーを定義します。
public partial class CustomTransportPage : ContentPage
{
...
void OnPlayPauseButtonClicked(object sender, EventArgs args)
{
if (video.Status == VideoStatus.Playing)
{
video.Pause();
}
else if (video.Status == VideoStatus.Paused)
{
video.Play();
}
}
void OnStopButtonClicked(object sender, EventArgs args)
{
video.Stop();
}
...
}
カスタム位置バー
次の例は、XAML で使用されるカスタム位置バー PositionSlider を示しています。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.CustomPositionBarPage"
Unloaded="OnContentPageUnloaded"
Title="Custom position bar">
<Grid x:DataType="controls:Video"
RowDefinitions="*,Auto,Auto">
<controls:Video x:Name="video"
AreTransportControlsEnabled="False"
Source="{StaticResource ElephantsDream}" />
...
<Grid Grid.Row="1"
Margin="10,0"
ColumnDefinitions="0.25*,0.25*,0.25*,0.25*"
BindingContext="{x:Reference video}">
<Label Text="{Binding Path=Position,
StringFormat='{0:hh\\:mm\\:ss}'}"
HorizontalOptions="Center"
VerticalOptions="Center" />
...
<Label Grid.Column="3"
Text="{Binding Path=TimeToEnd,
StringFormat='{0:hh\\:mm\\:ss}'}"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
<controls:PositionSlider Grid.Row="2"
Margin="10,0,10,10"
BindingContext="{x:Reference video}"
Duration="{Binding Duration}"
Position="{Binding Position}">
<controls:PositionSlider.Triggers>
<DataTrigger TargetType="controls:PositionSlider"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsEnabled"
Value="False" />
</DataTrigger>
</controls:PositionSlider.Triggers>
</controls:PositionSlider>
</Grid>
</ContentPage>
Position オブジェクトの Video プロパティは、Position の PositionSlider プロパティにバインドされますが、Video.Position プロパティは MauiVideoPlayer.UpdateStatus メソッドによって変更されるため、パフォーマンスの問題は発生しません。 各プラットフォームは 1 秒間に 10 回しか呼び出されません。 さらに、2 つの Label オブジェクトは、Position オブジェクトの TimeToEnd プロパティ値と Video プロパティ値を表示します。
ネイティブ ビュー クリーンアップ
各プラットフォームのハンドラー実装は、DisconnectHandler 実装をオーバーライドします。この実装は、イベントのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行するために使用されます。 ただし、このオーバーライドは、.NET MAUI によって意図的に呼び出されません。 代わりに、アプリのライフサイクル内の適切な場所から自分で呼び出す必要があります。 これは多くの場合、Video コントロールを含むページから移動し、ページの Unloaded イベントが発生する場合に原因となります。
ページの Unloaded イベントのイベント ハンドラーは、XAML で登録できます。
<ContentPage ...
xmlns:controls="clr-namespace:VideoDemos.Controls"
Unloaded="OnContentPageUnloaded">
<controls:Video x:Name="video"
... />
</ContentPage>
Unloaded イベントのイベント ハンドラーは、DisconnectHandler インスタンスで Handler メソッドを呼び出すことができます。
void OnContentPageUnloaded(object sender, EventArgs e)
{
video.Handler?.DisconnectHandler();
}
ネイティブ ビュー リソースのクリーン アップに加えて、ハンドラーの DisconnectHandler メソッドを呼び出すと、iOS での逆方向ナビゲーションでのビデオの再生も確実に停止します。
制御ハンドラーの切断
各プラットフォームのハンドラー実装は、DisconnectHandler 実装をオーバーライドします。この実装は、イベントのサブスクライブ解除やオブジェクトの破棄など、ネイティブ ビューのクリーンアップを実行するために使用されます。 通常、ハンドラーはアプリ内で後ろに移動するときなど、可能な場合には既定で自動的にコントロールから切断されます。
一部のシナリオでは、ハンドラーがコントロールから切断されるタイミングを制御する必要があります。これは、 HandlerProperties.DisconnectPolicy 添付プロパティを使用して実現できます。 このプロパティには、次の値を定義する列挙体を含む HandlerDisconnectPolicy 引数が必要です。
-
Automaticは、ハンドラーが自動的に切断されることを示します。 これはHandlerProperties.DisconnectPolicy添付プロパティの既定値です。 -
Manualは、 DisconnectHandler() 実装を呼び出すことによってハンドラーを手動で切断する必要があることを示します。
次の例では、HandlerProperties.DisconnectPolicy 添付プロパティを設定しています。
<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />
同等の C# コードを次に示します。
Video video = new Video
{
Source = "video.mp4",
AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video, HandlerDisconnectPolicy.Manual);
HandlerProperties.DisconnectPolicy添付プロパティをManualに設定するときは、アプリのライフサイクル内の適切な場所からハンドラーのDisconnectHandler実装を自分で呼び出す必要があります。 これは、 video.Handler?.DisconnectHandler();を呼び出すことによって実現できます。
さらに、指定されたDisconnectHandlersのハンドラーを切断するためのIView拡張メソッドが追加されています。
video.DisconnectHandlers();
切断すると、完了するか、DisconnectHandlers メソッドはコントロール ツリーの下位要素へ順に伝播し、処理が完了するか、手動ポリシーを設定しているコントロールに到達するまで呼び出されます。
.NET MAUI
サンプルを参照する