使用處理程式建立自定義控制件
應用程式的標準需求是播放影片的能力。 本文探討如何建立 .NET 多平臺應用程式 UI (.NET MAUI) 跨平臺 Video
控件,以使用處理程式將跨平臺控件 API 對應至播放影片的 Android、iOS 和 Mac Catalyst 上的原生檢視。 此控制元件可以從三個來源播放影片:
- 表示遠端視訊的 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
為每個平臺建立處理程序類別,以建立實作跨平臺控件的原生檢視。 如需詳細資訊,請參閱 建立平臺控件。- 在應用程式的
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
語句定義 PlatformView
等於 System.Object
。 這是必要的,如此一來, PlatformView
類型就可以在處理程式內用於所有平臺。 替代方式是必須使用條件式編譯,在每個平臺定義 PlatformView
屬性一次。
建立屬性對應程式
每個處理程式通常會提供 屬性對應程式,定義跨平臺控制件中發生屬性變更時要採取的動作。 類型 PropertyMapper 是 Dictionary
,會將跨平臺控件的屬性對應至其相關聯的 Actions。
PropertyMapper 定義在 .NET MAUI 的 ViewHandler<TVirtualView,TPlatformView> 類別中,而且需要提供兩個泛型自變數:
- 跨平臺控制項的 類別,其衍生自 View。
- 處理程序的類別。
下列程式代碼範例顯示 VideoHandler
使用 定義擴充的 PropertyMapper 類別:
public partial class VideoHandler
{
public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
{
[nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
[nameof(Video.Source)] = MapSource,
[nameof(Video.IsLooping)] = MapIsLooping,
[nameof(Video.Position)] = MapPosition
};
public VideoHandler() : base(PropertyMapper)
{
}
}
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> 類別中,而且需要提供兩個泛型自變數:
- 跨平臺控制項的 類別,其衍生自 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。 這可確保從跨平臺控件傳送命令時,將會視需要操作基礎原生檢視。 這種方法的優點是,它不需要原生檢視訂閱和取消訂閱跨平臺控件事件。 此外,它允許輕鬆自定義,因為命令對應程式可以由跨平臺控件取用者修改,而不需要子類別化。
建立平臺控制件
建立處理程式的對應器之後,您必須在所有平臺上提供處理程序實作。 這可以藉由在 [平臺] 資料夾的子資料夾中新增部分類別處理程式實作來完成。 或者,您可以設定專案以支援以檔名為基礎的多重目標,或資料夾型多重目標,或兩者。
範例應用程式設定為支援以檔名為基礎的多重目標,讓處理程序類別全都位於單一資料夾中:
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> 類別,這需要兩個類型自變數:
- 跨平臺控制項的 類別,其衍生自 View。
- 在平台上實作跨平臺控件的原生檢視類型。 這應該與處理程式中的屬性類型
PlatformView
相同。
重要
類別 ViewHandler<TVirtualView,TPlatformView> 提供 VirtualView 和 PlatformView 屬性。 屬性 VirtualView 可用來從其處理程式存取跨平臺控制件。 屬性 PlatformView 可用來存取實作跨平臺控件之每個平臺上的原生檢視。
每個平台處理程式實作都應該覆寫下列方法:
- CreatePlatformView,其應該建立並傳回實作跨平臺控件的原生檢視。
- ConnectHandler,其應該執行任何原生檢視設定,例如初始化原生檢視和執行事件訂閱。
- DisconnectHandler,其應該執行任何原生檢視清除,例如取消訂閱事件和處置物件。
重要
方法 DisconnectHandler 不會由 .NET MAUI 刻意叫用。 相反地,您必須從應用程式生命週期中的適當位置自行叫用它。 如需詳細資訊,請參閱 原生檢視清除。
重要
DisconnectHandler根據預設,.NET MAUI 會自動叫用 方法,不過此行為可以變更。 如需詳細資訊,請參閱 控制處理程式中斷連線。
每個平台處理程式也應該實作對應程式字典中定義的動作。
此外,每個平台處理程式也應該視需要提供程式碼,以在平臺上實作跨平臺控件的功能。 或者,這可以由其他類型提供,這是這裡採用的方法。
Android
影片會在 Android VideoView
上使用 播放。 不過,在這裡,已封裝在類型中MauiVideoPlayer
,VideoView
讓原生檢視與其處理程式保持分隔。 下列範例顯示 VideoHandler
Android的部分類別,其三個覆寫:
#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(Context, VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
衍生自 ViewHandler<TVirtualView,TPlatformView> 類別,其中泛型 Video
自變數會指定跨平臺控件類型,以及 MauiVideoPlayer
指定封裝原生檢視的型別的 VideoView
自變數。
覆 CreatePlatformView 寫會建立並傳 MauiVideoPlayer
回物件。 覆 ConnectHandler 寫是執行任何必要原生檢視設定的位置。 覆DisconnectHandler寫是執行任何原生檢視清除的位置,因此會在 實例上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
需要處理程式和跨平臺控件實例做為自變數的方法。 在每個案例中,Action 會呼叫 類型中 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
需要處理程式和跨平臺控件實例,以及選擇性數據做為自變數的方法。 在每個案例中,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;
}
...
}
}
MauiVideoPlayer
衍生自 CoordinatorLayout
,因為 Android 上 .NET MAUI 應用程式中的根原生檢視是 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
叫用 方法,進而在 中MauiVideoPlayer
呼叫 UpdateTransportControlsEnabled
方法:
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
,以保持原生檢視與其處理程式分開。 下列範例顯示 VideoHandler
iOS 的部分類別,其三個覆寫:
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.MaciOS;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
衍生自 ViewHandler<TVirtualView,TPlatformView> 類別,其中泛型 Video
自變數會指定跨平臺控件類型,以及 MauiVideoPlayer
指定封裝 AVPlayer
和 AVPlayerViewController
原生檢視的型別的自變數。
覆 CreatePlatformView 寫會建立並傳 MauiVideoPlayer
回物件。 覆 ConnectHandler 寫是執行任何必要原生檢視設定的位置。 覆DisconnectHandler寫是執行任何原生檢視清除的位置,因此會在 實例上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
需要處理程式和跨平臺控件實例做為自變數的方法。 在每個案例中,Action 會呼叫 類型中 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
需要處理程式和跨平臺控件實例,以及選擇性數據做為自變數的方法。 在每個案例中,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
必須新增至 Shell 型應用程式的父 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
在覆寫中Dispose
將 設定為 null
,並執行其他原生檢視清除。
注意
由 Dispose
處理程式的 DisconnectHandler 覆寫呼叫覆寫。
平台傳輸控制件包含播放、暫停和停止視訊的按鈕,並由類型提供 AVPlayerViewController
。 Video.AreTransportControlsEnabled
如果屬性設定為 true
,則會AVPlayerViewController
顯示其播放控制件。 這是因為設定 屬性時AreTransportControlsEnabled
,處理程式的屬性對應程式會確保MapAreTransportControlsEnabled
叫用 方法,進而在 中MauiVideoPlayer
呼叫 UpdateTransportControlsEnabled
方法:
public class MauiVideoPlayer : UIView
{
AVPlayerViewController _playerViewController;
Video _video;
...
public void UpdateTransportControlsEnabled()
{
_playerViewController.ShowsPlaybackControls = _video.AreTransportControlsEnabled;
}
...
}
如果傳輸控制項未使用,但可藉由點選視訊來還原,傳輸控件就會淡出。
Video.AreTransportControlsEnabled
如果屬性設定為 false
,則 AVPlayerViewController
不會顯示其播放控制件。 在此案例中,您可以透過程序設計方式控制視訊播放,或提供您自己的傳輸控制項。 如需詳細資訊,請參閱 建立自定義傳輸控制件。
Windows
視訊會在 Windows MediaPlayerElement
上使用 播放。 不過,在這裡,已封裝在類型中MauiVideoPlayer
,MediaPlayerElement
讓原生檢視與其處理程式保持分隔。 下列範例顯示 VideoHandler
Windows 的部分類別,其三個覆寫:
#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Windows;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
衍生自 ViewHandler<TVirtualView,TPlatformView> 類別,其中泛型 Video
自變數會指定跨平臺控件類型,以及 MauiVideoPlayer
指定封裝原生檢視的型別的 MediaPlayerElement
自變數。
覆 CreatePlatformView 寫會建立並傳 MauiVideoPlayer
回物件。 覆 ConnectHandler 寫是執行任何必要原生檢視設定的位置。 覆DisconnectHandler寫是執行任何原生檢視清除的位置,因此會在 實例上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
需要處理程式和跨平臺控件實例做為自變數的方法。 在每個案例中,Action 會呼叫 類型中 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
需要處理程式和跨平臺控件實例,以及選擇性數據做為自變數的方法。 在每個案例中,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
叫用 方法,進而在 中MauiVideoPlayer
呼叫 UpdateTransportControlsEnabled
方法:
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);
...
}
...
}
}
在此範例中 VideoHandler
,在 Android 的部分類別中, MapSource
方法會將 Video
實例 MauiVideoPlayer
轉換成 物件。
方法 ToPlatform 也可以從跨平台程序代碼,將跨平臺控制項轉換成其基礎平臺控制項:
using Microsoft.Maui.Platform;
namespace VideoDemos.Views;
public partial class MyPage : ContentPage
{
...
protected override void OnHandlerChanged()
{
// Convert cross-platform control to its underlying platform control
#if ANDROID
Android.Views.View nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif IOS || MACCATALYST
UIKit.UIView nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif WINDOWS
Microsoft.UI.Xaml.FrameworkElement nativeView = video.ToPlatform(video.Handler.MauiContext);
#endif
...
}
...
}
在此範例中,名為 video
的跨平臺Video
控件會轉換成覆寫中OnHandlerChanged()每個平臺上的基礎原生檢視。 當實作跨平臺控件的原生檢視可供使用並初始化時,就會呼叫此覆寫。 方法傳 ToPlatform 回的物件可以轉換成其確切的原生類型,這裡為 MauiVideoPlayer
。
播放影片
類別 Video
會 Source
定義屬性,用來指定視訊檔案的來源和 AutoPlay
屬性。 AutoPlay
默認為 true
,這表示影片應該在設定之後 Source
自動開始播放。 如需這些屬性的定義,請參閱 建立跨平臺控件。
屬性 Source
的類型為 VideoSource
,這個抽象類是由三個靜態方法所組成,可具現化衍生自 VideoSource
的三個類別:
using System.ComponentModel;
namespace VideoDemos.Controls
{
[TypeConverter(typeof(VideoSourceConverter))]
public abstract class VideoSource : Element
{
public static VideoSource FromUri(string uri)
{
return new UriVideoSource { Uri = uri };
}
public static VideoSource FromFile(string file)
{
return new FileVideoSource { File = file };
}
public static VideoSource FromResource(string path)
{
return new ResourceVideoSource { Path = path };
}
}
}
VideoSource
類別包含一個 TypeConverter
屬性,該屬性會參考 VideoSourceConverter
:
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class VideoSourceConverter : TypeConverter, IExtendedTypeConverter
{
object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
{
if (!string.IsNullOrWhiteSpace(value))
{
Uri uri;
return Uri.TryCreate(value, UriKind.Absolute, out uri) && uri.Scheme != "file" ?
VideoSource.FromUri(value) : VideoSource.FromResource(value);
}
throw new InvalidOperationException("Cannot convert null or whitespace to VideoSource.");
}
}
}
當屬性設定為 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
上使用 播放。 下列程式代碼範例示範方法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
物件時, SetVideoUri
會使用的方法來 VideoView
指定要播放的視訊,以及從字元串 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 上的影片會在影片播放頁面流覽離開之後繼續播放。 若要停止影片,則會ReplaceCurrentItemWithPlayerItem
在覆寫中Dispose
設定為 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
屬性會設定為 MediaSource
物件,這個物件會使用要播放之視訊的 URI 初始化 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
下列程式代碼範例示範方法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
物件時, SetVideoPath
會使用的方法來 VideoView
指定要播放的視訊,並使用字元串自變數結合應用程式的套件名稱與影片的檔名。
資源影片檔案會儲存在套件 的 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
別的物件時, GetUrlForResource
的方法 NSBundle
會用來從應用程式套件擷取檔案。 完整路徑必須分割成檔案名稱、副檔名和目錄。
在某些情況下,iOS 上的影片會在影片播放頁面流覽離開之後繼續播放。 若要停止影片,則會ReplaceCurrentItemWithPlayerItem
在覆寫中Dispose
設定為 null
:
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
可用來存取裝置影片庫中的影片。 它會定義 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
下列程式代碼範例示範方法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
別的物件時, SetVideoPath
會使用 的方法來 VideoView
指定要播放的視訊檔案。
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
物件時,視訊檔名會 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
觀察者,也會在 上NSObject
叫Dispose
用 方法。
Windows
下列程式代碼範例示範 Windows 上的 方法如何 UpdateIsLooping
啟用視訊迴圈:
public void UpdateIsLooping()
{
if (_isMediaPlayerAttached)
_mediaPlayerElement.MediaPlayer.IsLoopingEnabled = _video.IsLooping;
}
若要啟用視訊迴圈,方法會將 UpdateIsLooping
MediaPlayerElement.MediaPlayer.IsLoopingEnabled
屬性設定為 屬性的值 Video.IsLooping
。
建立自訂傳輸控制項
視訊播放程式傳輸控制件包含播放、暫停和停止視訊的按鈕。 這些按鈕通常會以熟悉的圖示而非文字來識別,而且播放和暫停按鈕通常會合併成一個按鈕。
根據預設, Video
控件會顯示每個平臺支援的傳輸控制件。 不過,當您將 屬性設定為 AreTransportControlsEnabled
false
時,會隱藏這些控件。 然後,您可以透過程序設計方式控制視訊播放,或提供自己的傳輸控件。
實作您自己的傳輸控制項時, Video
類別必須能夠通知其原生檢視播放、暫停或停止視訊,以及知道影片播放的目前狀態。 類別 Video
會定義名為 Play
、 Pause
和 的方法,該 Stop
方法會引發對應的事件,並將命令傳送至 VideoHandler
:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public event EventHandler<VideoPositionEventArgs> PlayRequested;
public event EventHandler<VideoPositionEventArgs> PauseRequested;
public event EventHandler<VideoPositionEventArgs> StopRequested;
public void Play()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PlayRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PlayRequested), args);
}
public void Pause()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PauseRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PauseRequested), args);
}
public void Stop()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
StopRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.StopRequested), args);
}
}
}
類別 VideoPositionEventArgs
會 Position
定義可透過其建構函式設定的屬性。 這個屬性代表影片播放開始、暫停或停止的位置。
、 與方法中的Play
最後一行會將命令與相關聯資料傳送至 VideoHandler
Stop
Pause
的 VideoHandler
,會將CommandMapper命令名稱對應至收到命令時執行的動作。 例如,當收到 PlayRequested
命令時VideoHandler
,它會執行其 MapPlayRequested
方法。 這種方法的優點是,它不需要原生檢視訂閱和取消訂閱跨平臺控件事件。 此外,它允許輕鬆自定義,因為命令對應程式可以由跨平臺控件取用者修改,而不需要子類別化。 如需 的詳細資訊 CommandMapper,請參閱 建立命令對應程式。
MauiVideoPlayer
Android、iOS 和 Mac Catalyst 上的實作具有PlayRequested
、 PauseRequested
和 StopRequested
方法來回應傳送 PlayRequested
、 PauseRequested
和 StopRequested
命令的Video
控制項。 每個方法都會在其原生檢視上叫用方法,以播放、暫停或停止影片。 例如,下列程式代碼會顯示 PlayRequested
iOS 和 Mac Catalyst 上的、 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}.");
}
}
}
這三種方法都會使用隨 命令傳送的數據,記錄影片播放、暫停或停止的位置。
此機制可確保在控件上Video
叫用、 Pause
或 Stop
方法時Play
,會指示其原生檢視播放、暫停或停止視訊,並記錄影片播放、暫停或停止的位置。 這一切都會使用分離的方法進行,而不需要原生檢視訂閱跨平臺事件。
視訊狀態
實作播放、暫停和停止功能並不足以支援自定義傳輸控制件。 通常應該使用相同的按鈕來實作播放和暫停功能,這會變更其外觀,以指出影片目前正在播放或暫停。 此外,如果影片尚未載入,甚至不應該啟用按鈕。
這些需求指出影片播放程式需要提供目前狀態,指出其正在播放、暫停或仍未準備好播放影片。 此狀態可以透過列舉來表示:
public enum VideoStatus
{
NotReady,
Playing,
Paused
}
類別 Video
會定義名為 Status
類型的 VideoStatus
唯讀可系結屬性。 此屬性定義為唯讀,因為它只應該從控制件的處理程式設定:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey StatusPropertyKey =
BindableProperty.CreateReadOnly(nameof(Status), typeof(VideoStatus), typeof(Video), VideoStatus.NotReady);
public static readonly BindableProperty StatusProperty = StatusPropertyKey.BindableProperty;
public VideoStatus Status
{
get { return (VideoStatus)GetValue(StatusProperty); }
}
VideoStatus IVideoController.Status
{
get { return Status; }
set { SetValue(StatusPropertyKey, value); }
}
...
}
}
通常,唯讀可繫結屬性擁有 Status
屬性上專用的 set
存取子,使其可以從類別內設定。 不過,對於 View 處理程式所支援的衍生專案,屬性必須從類別外部設定,但只能由控件的處理程式設定。
因此,會為另一個屬性定義名稱 IVideoController.Status
。 此為明確介面實作,且 Video
類別實作的 IVideoController
介面使其變得可行:
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
這個介面可讓外部的類別藉由參考 IVideoController
介面來Video
設定 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
事件處理程式會每隔十秒執行一次,這會引發 UpdateStatus
事件並叫用 UpdateStatus
命令。
UpdateStatus
當命令從 Video
控制項傳送至其處理程式時,處理程式的命令對應程式可確保MapUpdateStatus
叫用 方法:
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
方法 MapUpdateStatus
會輪流呼叫 UpdateStatus
處理程序 PlatformView
屬性上的方法。 PlatformView
屬於類型的 MauiVideoPlayer
屬性會封裝原生檢視,以提供每個平臺上的視訊播放程序實作。
Android
下列程式代碼範例顯示 UpdateStatus
Android 上的 方法會設定 Status
屬性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public MauiVideoPlayer(Context context, Video video) : base(context)
{
_video = video;
...
_videoView.Prepared += OnVideoViewPrepared;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_videoView.Prepared -= OnVideoViewPrepared;
...
}
base.Dispose(disposing);
}
void OnVideoViewPrepared(object sender, EventArgs args)
{
_isPrepared = true;
((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
}
public void UpdateStatus()
{
VideoStatus status = VideoStatus.NotReady;
if (_isPrepared)
status = _videoView.IsPlaying ? VideoStatus.Playing : VideoStatus.Paused;
((IVideoController)_video).Status = status;
...
}
...
}
}
屬性 VideoView.IsPlaying
是布爾值,指出影片是否正在播放或暫停。 若要判斷 VideoView
無法播放或暫停視訊,必須處理其 Prepared
事件。 當媒體來源準備好播放時,就會引發此事件。 事件會在建構函式中 MauiVideoPlayer
訂閱,並在其 Dispose
覆寫中取消訂閱。 然後,方法UpdateStatus
會使用 isPrepared
欄位和 VideoView.IsPlaying
屬性,將物件上的屬性轉換成 IVideoController
來設定 Status
屬性Video
。
iOS 和 Mac Catalyst
下列程式代碼範例顯示 UpdateStatus
iOS 和 Mac Catalyst 上的 方法會設定 Status
屬性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
Video _video;
...
public void UpdateStatus()
{
VideoStatus videoStatus = VideoStatus.NotReady;
switch (_player.Status)
{
case AVPlayerStatus.ReadyToPlay:
switch (_player.TimeControlStatus)
{
case AVPlayerTimeControlStatus.Playing:
videoStatus = VideoStatus.Playing;
break;
case AVPlayerTimeControlStatus.Paused:
videoStatus = VideoStatus.Paused;
break;
}
break;
}
((IVideoController)_video).Status = videoStatus;
...
}
...
}
}
必須存取 的 AVPlayer
兩個屬性,才能設定 Status
屬性 - Status
型 AVPlayerStatus
別的 屬性和 TimeControlStatus
類型的 AVPlayerTimeControlStatus
屬性。 Status
然後,可以將屬性轉換成 ,以在 對象上Video
設定屬性IVideoController
。
Windows
下列程式代碼範例顯示 UpdateStatus
Windows 上的 方法會設定 Status
屬性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateStatus()
{
if (_isMediaPlayerAttached)
{
VideoStatus status = VideoStatus.NotReady;
switch (_mediaPlayerElement.MediaPlayer.CurrentState)
{
case MediaPlayerState.Playing:
status = VideoStatus.Playing;
break;
case MediaPlayerState.Paused:
case MediaPlayerState.Stopped:
status = VideoStatus.Paused;
break;
}
((IVideoController)_video).Status = status;
_video.Position = _mediaPlayerElement.MediaPlayer.Position;
}
}
...
}
}
方法 UpdateStatus
會使用 屬性的值 MediaPlayerElement.MediaPlayer.CurrentState
來判斷 屬性的值 Status
。 Status
然後,可以將屬性轉換成 ,以在 對象上Video
設定屬性IVideoController
。
定位列
每個平臺所實作的傳輸控制件都包含定位列。 此列類似於滑桿或滾動條,並在影片的總持續時間內顯示影片的目前位置。 用戶可以操作定位列,以向前或向後移動至影片中的新位置。
實作您自己的定位列需要 Video
類別知道影片的持續時間,以及該持續時間內的目前位置。
期間
控件需要支援自定義定位列的其中一個專案 Video
是影片的持續時間。 類別 Video
會定義名為 Duration
的唯讀可系結屬性,類型為 TimeSpan
。 此屬性定義為唯讀,因為它只應該從控制件的處理程式設定:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey DurationPropertyKey =
BindableProperty.CreateReadOnly(nameof(Duration), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public static readonly BindableProperty DurationProperty = DurationPropertyKey.BindableProperty;
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
}
TimeSpan IVideoController.Duration
{
get { return Duration; }
set { SetValue(DurationPropertyKey, value); }
}
...
}
}
通常,唯讀可繫結屬性擁有 Duration
屬性上專用的 set
存取子,使其可以從類別內設定。 不過,對於 View 處理程式所支援的衍生專案,屬性必須從類別外部設定,但只能由控件的處理程式設定。
注意
可系結屬性的屬性變更事件處理程式Duration
會呼叫名為 SetTimeToEnd
的方法,如計算結束時間中所述。
因此,會為另一個屬性定義名稱 IVideoController.Duration
。 此為明確介面實作,且 Video
類別實作的 IVideoController
介面使其變得可行:
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
這個介面可讓外部的類別藉由參考 IVideoController
介面來Video
設定 Duration
屬性。 屬性可以從其他類別和處理程序設定,但不太可能不小心設定。 最重要的是, 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
立即取得。 可以設定 屬性的 Duration
iOS 觀察者,但 類別 MauiVideoPlayer
會取得方法中的 UpdateStatus
持續時間,該方法每秒呼叫 10 次:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayerItem _playerItem;
...
TimeSpan ConvertTime(CMTime cmTime)
{
return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
}
public void UpdateStatus()
{
...
if (_playerItem != null)
{
((IVideoController)_video).Duration = ConvertTime(_playerItem.Duration);
...
}
}
...
}
}
ConvertTime
方法會將 CMTime
物件轉換為 TimeSpan
值。
Windows
在 Windows 上 MediaPlayerElement.MediaPlayer.NaturalDuration
,屬性是一個 TimeSpan
值,會在引發事件時 MediaPlayerElement.MediaPlayer.MediaOpened
變成有效。 類別 MauiVideoPlayer
會 MediaOpened
使用 事件處理程式來取得 NaturalDuration
屬性值:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
void OnMediaPlayerMediaOpened(MediaPlayer sender, object args)
{
MainThread.BeginInvokeOnMainThread(() =>
{
((IVideoController)_video).Duration = _mediaPlayerElement.MediaPlayer.NaturalDuration;
});
}
...
}
}
OnMediaPlayer
然後,事件處理程式會呼叫 MainThread.BeginInvokeOnMainThread
方法,在主線程上將它IVideoController
轉換成 ,以在 對象上Video
設定 Duration
屬性。 這是必要的,因為 MediaPlayerElement.MediaPlayer.MediaOpened
事件是在背景線程上處理。 如需在主線程上執行程式碼的詳細資訊,請參閱 在 .NET MAUI UI 線程上建立線程。
Position
控件 Video
也需要 Position
從零 Duration
增加到視訊播放時的屬性。 類別會將 Video
此屬性實作為具有公用 get
和 set
存取子的可系結屬性:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public static readonly BindableProperty PositionProperty =
BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public TimeSpan Position
{
get { return (TimeSpan)GetValue(PositionProperty); }
set { SetValue(PositionProperty, value); }
}
...
}
}
存取 get
子會傳回影片的目前播放位置。 存取 set
子會透過向前或向後移動視訊位置,回應使用者操作定位列。
注意
可系結屬性的屬性變更事件處理程式Position
會呼叫名為 SetTimeToEnd
的方法,如計算結束時間中所述。
在 Android、iOS 和 Mac Catalyst 上,取得目前位置的屬性只有存取 get
子。 相反地, Seek
方法可用來設定位置。 這似乎比使用具有固有問題的單 Position
一屬性更合理。 視訊播放時, Position
必須持續更新屬性以反映新位置。 但是您不希望屬性的大部分 Position
變更導致視訊播放程式移至影片中的新位置。 如果發生這種情況,影片播放程式會搜尋 Position
屬性的最後一個值來回應,因此影片不會前進。
儘管使用 和 set
存取子實Position
作屬性get
時發生困難,但此方法仍會使用,因為它可以利用數據系結。 控制元件 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
只有在 屬性與 目前位置VideoView
之間的差異Position
大於一秒時,才會在物件上VideoView
呼叫 Seek
方法。
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
只有在 屬性與 目前位置AVPlayer
之間的差異Position
大於一秒時,才會在物件上AVPlayer
呼叫 Seek
方法。
Windows
在 Windows 上 MediaPlayerElement.MedaPlayer.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
之間的差異Position
大於一秒時,才會設定 MediaPlayerElement.MediaPlayer.Position
屬性。
計算結束時間
有時影片播放程式會顯示影片中的剩餘時間。 此值會從影片開始的持續時間開始,並在影片結束時減少為零。
類別Video
包含唯讀TimeToEnd
屬性,其會根據 和 Position
屬性的Duration
變更來計算:
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
和 Position
屬性的屬性Duration
變更事件處理程式呼叫 方法。
自定義定位列
您可以藉由建立衍生自 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
的 Slider 屬性設定Maximum
為 TotalSeconds
值的 屬性TimeSpan
。 同樣地,屬性的屬性變更事件處理程序 Position
會設定 Value
的 Slider屬性。 這是追蹤 位置PositionSlider
的機制Slider。
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 是跨平臺控件類型,第二個自變數是其處理程序類型。
取用跨平臺控制件
向您的應用程式註冊處理程序之後,就可以取用跨平臺控制件。
播放 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
的字串轉換成 。 影片接著會開始載入,並在下載並緩衝處理足夠的數據量後開始播放。 在每個平臺上,如果傳輸控件未使用,但可以點選視訊來還原,傳輸控件就會淡出。
播放影片資源
控件可以使用 Video
MauiAsset 建置動作,內嵌在應用程式的 Resources\Raw 資料夾中的視訊檔案:
<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" />
如果您將 和 AreTransportControlsEnabled
設定AutoPlay
為 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
對象的 屬性會系結至 Position
的 PositionSlider
屬性Video
,而不會發生效能問題,因為 Video.Position
屬性會由MauiVideoPlayer.UpdateStatus
每個平臺上的 方法變更,而這個屬性只會每秒呼叫 10 次。 此外,兩個 Label 對象會顯示 Position
來自物件的 和 TimeToEnd
屬性值 Video
。
原生檢視清除
每個平台的處理程式實作都會 DisconnectHandler 覆寫 實作,用來執行原生檢視清除,例如取消訂閱事件和處置物件。 不過,此覆寫是故意不會由 .NET MAUI 叫用。 相反地,您必須從應用程式生命週期中的適當位置自行叫用它。 這通常是當包含 Video
控件的頁面巡覽離開時,這會導致引發頁面的事件 Unloaded
。
頁面 Unloaded
事件的事件處理程式可以在 XAML 中註冊:
<ContentPage ...
xmlns:controls="clr-namespace:VideoDemos.Controls"
Unloaded="OnContentPageUnloaded">
<controls:Video x:Name="video"
... />
</ContentPage>
然後,Unloaded
事件的事件處理程式就可以在其Handler
實例上叫DisconnectHandler用 方法:
void OnContentPageUnloaded(object sender, EventArgs e)
{
video.Handler?.DisconnectHandler();
}
除了清除原生檢視資源之外,叫用處理程式的 DisconnectHandler 方法也可確保影片停止在iOS上的向後流覽播放。
控制處理程式中斷連線
每個平台的處理程式實作都會 DisconnectHandler 覆寫 實作,用來執行原生檢視清除,例如取消訂閱事件和處置物件。 根據預設,處理程式會盡可能自動中斷與其控件的連線,例如在應用程式中向後巡覽時。
在某些情況下,您可能會想要控制處理程式何時與控件中斷連線,這可以使用附加屬性來 HandlerProperties.DisconnectPolicy
達成。 此屬性需要自 HandlerDisconnectPolicy 變數,且列舉定義下列值:
Automatic
,表示處理程式會自動中斷連線。 這是HandlerProperties.DisconnectPolicy
附加屬性的預設值。Manual
,表示叫用 DisconnectHandler() 實作時,處理程式必須手動中斷連線。
下列範例顯示設定 HandlerProperties.DisconnectPolicy
附加屬性:
<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />
對等的 C# 程式碼為:
Video video = new Video
{
Source = "video.mp4",
AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video, HandlerDisconnectPolicy.Manual);
將附加屬性設定 HandlerProperties.DisconnectPolicy
為 Manual
時,您必須從應用程式生命週期中的適當位置自行叫用處理程式 DisconnectHandler 的實作。 這可以藉由叫 video.Handler?.DisconnectHandler();
用 來達成。
此外,還有一個 DisconnectHandlers 擴充方法可將處理程式與指定的 IView中斷連接:
video.DisconnectHandlers();
中斷連線時, DisconnectHandlers 方法會傳播到控件樹狀結構,直到它完成或到達已設定手動原則的控件為止。