共用方式為


使用處理程式建立自定義控制件

流覽範例。 流覽範例

應用程式的標準需求是播放影片的能力。 本文探討如何建立 .NET 多平臺應用程式 UI (.NET MAUI) 跨平臺 Video 控件,以使用處理程式將跨平臺控件 API 對應至播放影片的 Android、iOS 和 Mac Catalyst 上的原生檢視。 此控制元件可以從三個來源播放影片:

  • 表示遠端視訊的 URL。
  • 資源,這是內嵌在應用程式中的檔案。
  • 來自裝置影片庫的檔案。

視訊控件需要 傳輸控件,這些控件是播放和暫停視訊的按鈕,以及顯示影片進度的定位列,可讓使用者快速移至不同的位置。 控制項 Video 可以使用平臺所提供的傳輸控制項和定位列,也可以提供自定義傳輸控制項和定位列。 下列螢幕快照顯示 iOS 上的控制項,以及沒有自訂傳輸控制項:

iOS 上影片播放的螢幕快照。在 iOS 上使用自訂傳輸控制件播放影片的螢幕快照。

更複雜的視訊控件會有額外的功能,例如音量控件、在收到通話時中斷視訊播放的機制,以及在播放期間讓螢幕保持作用中的方式。

下圖顯示控制元件的 Video 架構:

視訊處理程序架構。

類別 Video 會提供控件的跨平臺 API。 跨平臺 API 與原生檢視 API 的對應是由 VideoHandler 每個平臺上的 類別所執行,而該類別會 Video 對應至 MauiVideoPlayer 類別。 在 iOS 和 Mac Catalyst 上,類別 MauiVideoPlayer 會使用 AVPlayer 類型來提供視訊播放。 在 Android 上,類別 MauiVideoPlayer 會使用 VideoView 類型來提供視訊播放。 在 Windows 上,類別 MauiVideoPlayer 會使用 MediaPlayerElement 類型來提供視訊播放。

重要

.NET MAUI 會透過介面將其處理程式與其跨平臺控件分離。 這可讓 Comet 和 Fabulous 等實驗性架構提供自己的跨平臺控件,以實作介面,同時仍使用 .NET MAUI 的處理程式。 只有在您需要將處理程式與其跨平臺控件分離,以達到類似目的,或基於測試目的,才需要建立跨平臺控件的介面。

建立跨平臺 .NET MAUI 自定義控件的程式,其平台實作是由處理程式提供,如下所示:

  1. 建立跨平臺控制項的類別,以提供控制件的公用 API。 如需詳細資訊,請參閱 建立跨平臺控件
  2. 建立任何必要的跨平台類型。
  3. 建立 partial 處理程序類別。 如需詳細資訊,請參閱 建立處理程式
  4. 在處理程式類別中,建立 PropertyMapper 字典,以定義在發生跨平臺屬性變更時要採取的動作。 如需詳細資訊,請參閱 建立屬性對應程式
  5. 或者,在您的處理程序類別中,建立 CommandMapper 字典,定義跨平臺控件傳送指令至實作跨平臺控制件的原生檢視時所要採取的動作。 如需詳細資訊,請參閱 建立命令對應程式
  6. partial為每個平臺建立處理程序類別,以建立實作跨平臺控件的原生檢視。 如需詳細資訊,請參閱 建立平臺控件
  7. 在應用程式的 MauiProgram 類別中使用 ConfigureMauiHandlersAddHandler 方法註冊處理程式。 如需詳細資訊,請參閱 註冊處理程式

然後,可以取用跨平臺控件。 如需詳細資訊,請參閱 取用跨平臺控件

建立跨平臺控制件

若要建立跨平臺控件,您應該建立衍生自 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 屬性一次。

建立屬性對應程式

每個處理程式通常會提供 屬性對應程式,定義跨平臺控制件中發生屬性變更時要採取的動作。 類型 PropertyMapperDictionary ,會將跨平臺控件的屬性對應至其相關聯的 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)
    {
    }
}

PropertyMapperDictionary ,其索引鍵為 ,string其值為泛型 Actionstring表示跨平臺控件的屬性名稱,而 Action 表示static需要處理程式和跨平臺控件做為自變數的方法。 例如,方法的 MapSource 簽章是 public static void MapSource(VideoHandler handler, Video video)

每個平臺處理程式都必須提供動作的實作,以操作原生檢視 API。 這可確保在跨平臺控件上設定屬性時,基礎原生檢視會視需要更新。 這種方法的優點是,它可讓您輕鬆進行跨平臺控件自定義,因為跨平臺控件取用者可以修改屬性對應程式,而不需要子類別化。

建立命令對應程式

每個處理程式也可以提供 命令對應程式,定義跨平臺控制項將命令傳送至原生檢視時要採取的動作。 命令對應器類似於屬性對應器,但允許傳遞其他數據。 在此內容中,命令是傳送至原生檢視的指令,以及選擇性地傳送至原生檢視的數據。 類型 CommandMapperDictionary ,會將跨平臺控件成員對應至其相關聯的動作。

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

CommandMapperDictionary ,其索引鍵為 ,string其值為泛型 Actionstring表示跨平臺控制件的命令名稱,而 Action 表示static需要處理程式、跨平臺控件和選擇性數據做為自變數的方法。 例如,方法的 MapPlayRequested 簽章是 public static void MapPlayRequested(VideoHandler handler, Video video, object? args)

每個平臺處理程式都必須提供動作的實作,以操作原生檢視 API。 這可確保從跨平臺控件傳送命令時,將會視需要操作基礎原生檢視。 這種方法的優點是,它不需要原生檢視訂閱和取消訂閱跨平臺控件事件。 此外,它允許輕鬆自定義,因為命令對應程式可以由跨平臺控件取用者修改,而不需要子類別化。

建立平臺控制件

建立處理程式的對應器之後,您必須在所有平臺上提供處理程序實作。 這可以藉由在 [平臺] 資料夾的子資料夾中新增部分類別處理程式實作來完成。 或者,您可以設定專案以支援以檔名為基礎的多重目標,或資料夾型多重目標,或兩者。

範例應用程式設定為支援以檔名為基礎的多重目標,讓處理程序類別全都位於單一資料夾中:

專案之 Handlers 資料夾中檔案的螢幕快照。

VideoHandler包含對應器的類別會命名為 VideoHandler.cs。 其平台實作位於 VideoHandler.Android.csVideoHandler.MaciOS.csVideoHandler.Windows.cs 檔案中。 這個以檔名為基礎的多重目標設定方式是將下列 XML 新增至專案檔,作為節點的 <Project> 子系:

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

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

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

如需設定多重目標的詳細資訊,請參閱 設定多重目標

每個平台處理程式類別都應該是部分類別,而且衍生自 ViewHandler<TVirtualView,TPlatformView> 類別,這需要兩個類型自變數:

  • 跨平臺控制項的 類別,其衍生自 View
  • 在平台上實作跨平臺控件的原生檢視類型。 這應該與處理程式中的屬性類型 PlatformView 相同。

重要

類別 ViewHandler<TVirtualView,TPlatformView> 提供 VirtualViewPlatformView 屬性。 屬性 VirtualView 可用來從其處理程式存取跨平臺控制件。 屬性 PlatformView 可用來存取實作跨平臺控件之每個平臺上的原生檢視。

每個平台處理程式實作都應該覆寫下列方法:

  • CreatePlatformView,其應該建立並傳回實作跨平臺控件的原生檢視。
  • ConnectHandler,其應該執行任何原生檢視設定,例如初始化原生檢視和執行事件訂閱。
  • DisconnectHandler,其應該執行任何原生檢視清除,例如取消訂閱事件和處置物件。

重要

方法 DisconnectHandler 不會由 .NET MAUI 刻意叫用。 相反地,您必須從應用程式生命週期中的適當位置自行叫用它。 如需詳細資訊,請參閱 原生檢視清除

重要

DisconnectHandler根據預設,.NET MAUI 會自動叫用 方法,不過此行為可以變更。 如需詳細資訊,請參閱 控制處理程式中斷連線

每個平台處理程式也應該實作對應程式字典中定義的動作。

此外,每個平台處理程式也應該視需要提供程式碼,以在平臺上實作跨平臺控件的功能。 或者,這可以由其他類型提供,這是這裡採用的方法。

Android

影片會在 Android VideoView上使用 播放。 不過,在這裡,已封裝在類型中MauiVideoPlayerVideoView讓原生檢視與其處理程式保持分隔。 下列範例顯示 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 上,類別 MauiVideoPlayerVideoView 封裝 ,以保持原生檢視與其處理程式分開:

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。 版面配置參數會設定在 RelativeLayoutVideoView 上,使 VideoView 置中於頁面中,並展開以填滿可用空間,同時維持其外觀比例。

建構函式也會訂閱 VideoView.Prepared 事件。 當影片準備好播放,且在覆寫中 Dispose 取消訂閱時,就會引發此事件:

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

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

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

除了取消 Prepared 訂閱事件之外,覆 Dispose 寫也會執行原生檢視清除。

注意

Dispose 處理程式的 DisconnectHandler 覆寫呼叫覆寫。

平臺傳輸控制件包含播放、暫停和停止視訊的按鈕,並由Android MediaController 類型提供。 Video.AreTransportControlsEnabled如果屬性設定為 trueMediaController則會將 設定為的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 指定封裝 AVPlayerAVPlayerViewController 原生檢視的型別的自變數。

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 上,類別 MauiVideoPlayerAVPlayer 封裝 和 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 建立 物件,該物件會管理媒體檔案的播放和計時,並將它設定為 PlayerAVPlayerViewController屬性值。 會顯示 AVPlayerViewControllerAVPlayer 的內容,並呈現傳輸控件和其他功能。 接著會設定控件的大小和位置,以確保影片會置中於頁面,並展開以填滿可用空間,同時維持其外觀比例。 在 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 覆寫呼叫覆寫。

平台傳輸控制件包含播放、暫停和停止視訊的按鈕,並由類型提供 AVPlayerViewControllerVideo.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上使用 播放。 不過,在這裡,已封裝在類型中MauiVideoPlayerMediaPlayerElement讓原生檢視與其處理程式保持分隔。 下列範例顯示 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 上,類別 MauiVideoPlayerMediaPlayerElement 封裝 ,以保持原生檢視與其處理程式分開:

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 覆寫呼叫覆寫。

平台傳輸控制件包含播放、暫停和停止視訊的按鈕,並由類型提供 MediaPlayerElementVideo.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

播放影片

類別 VideoSource 定義屬性,用來指定視訊檔案的來源和 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 初始化 UriMediaPlayerElement.Source設定 之後,OnMediaPlayerMediaOpened事件處理程式方法會針對 MediaPlayerElement.MediaPlayer.MediaOpened 事件註冊。 這個事件處理程式是用來設定 Duration 控件的 Video 屬性。

在 方法結束時 UpdateSourceVideo.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

迴圈影片

類別 VideoIsLooping 定義 屬性,這可讓控件在到達結束之後,自動將視訊位置設定為開始。 默認為 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觀察者,也會在 上NSObjectDispose用 方法。

Windows

下列程式代碼範例示範 Windows 上的 方法如何 UpdateIsLooping 啟用視訊迴圈:

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

若要啟用視訊迴圈,方法會將 UpdateIsLooping MediaPlayerElement.MediaPlayer.IsLoopingEnabled 屬性設定為 屬性的值 Video.IsLooping

建立自訂傳輸控制項

視訊播放程式傳輸控制件包含播放、暫停和停止視訊的按鈕。 這些按鈕通常會以熟悉的圖示而非文字來識別,而且播放和暫停按鈕通常會合併成一個按鈕。

根據預設, Video 控件會顯示每個平臺支援的傳輸控制件。 不過,當您將 屬性設定為 AreTransportControlsEnabled false時,會隱藏這些控件。 然後,您可以透過程序設計方式控制視訊播放,或提供自己的傳輸控件。

實作您自己的傳輸控制項時, Video 類別必須能夠通知其原生檢視播放、暫停或停止視訊,以及知道影片播放的目前狀態。 類別 Video 會定義名為 PlayPause和 的方法,該 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);
        }
    }
}

類別 VideoPositionEventArgsPosition 定義可透過其建構函式設定的屬性。 這個屬性代表影片播放開始、暫停或停止的位置。

、 與方法中的Play最後一行會將命令與相關聯資料傳送至 VideoHandlerStop PauseVideoHandler ,會將CommandMapper命令名稱對應至收到命令時執行的動作。 例如,當收到 PlayRequested 命令時VideoHandler,它會執行其 MapPlayRequested 方法。 這種方法的優點是,它不需要原生檢視訂閱和取消訂閱跨平臺控件事件。 此外,它允許輕鬆自定義,因為命令對應程式可以由跨平臺控件取用者修改,而不需要子類別化。 如需 的詳細資訊 CommandMapper,請參閱 建立命令對應程式

MauiVideoPlayer Android、iOS 和 Mac Catalyst 上的實作具有PlayRequestedPauseRequestedStopRequested 方法來回應傳送 PlayRequestedPauseRequestedStopRequested 命令的Video控制項。 每個方法都會在其原生檢視上叫用方法,以播放、暫停或停止影片。 例如,下列程式代碼會顯示 PlayRequestediOS 和 Mac Catalyst 上的、 PauseRequestedStopRequested 方法:

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叫用、 PauseStop 方法時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 更新,類別 VideoUpdateStatus 定義事件和命令:

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 屬性 - StatusAVPlayerStatus 別的 屬性和 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 來判斷 屬性的值 StatusStatus然後,可以將屬性轉換成 ,以在 對象上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 ,以毫秒為單位報告有效的持續時間。 類別 MauiVideoPlayerPrepared 使用 事件處理程式來取得 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 變成有效。 類別 MauiVideoPlayerMediaOpened 使用 事件處理程式來取得 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 此屬性實作為具有公用 getset 存取子的可系結屬性:

namespace VideoDemos.Controls
{
    public class Video : View, IVideoController
    {
        ...
        public static readonly BindableProperty PositionProperty =
            BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(Video), new TimeSpan(),
                propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());

        public TimeSpan Position
        {
            get { return (TimeSpan)GetValue(PositionProperty); }
            set { SetValue(PositionProperty, value); }
        }
        ...
    }
}

存取 get 子會傳回影片的目前播放位置。 存取 set 子會透過向前或向後移動視訊位置,回應使用者操作定位列。

注意

可系結屬性的屬性變更事件處理程式Position會呼叫名為 SetTimeToEnd的方法,如計算結束時間中所述

在 Android、iOS 和 Mac Catalyst 上,取得目前位置的屬性只有存取 get 子。 相反地, Seek 方法可用來設定位置。 這似乎比使用具有固有問題的單 Position 一屬性更合理。 視訊播放時, Position 必須持續更新屬性以反映新位置。 但是您不希望屬性的大部分 Position 變更導致視訊播放程式移至影片中的新位置。 如果發生這種情況,影片播放程式會搜尋 Position 屬性的最後一個值來回應,因此影片不會前進。

儘管使用 和 set 存取子實Position作屬性get時發生困難,但此方法仍會使用,因為它可以利用數據系結。 控制元件 PositionVideo 屬性可以繫結至 Slider 用來顯示位置及搜尋新位置的 。 不過,實作 Position 屬性時需要數個預防措施,以避免意見反應迴圈。

Android

在Android上 VideoView.CurrentPosition ,屬性會指出影片的目前位置。 類別MauiVideoPlayerPosition會在 方法中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);
            }
        }
        ...
    }
}

屬性每次 PositionUpdateStatus 方法設定時, Position 屬性都會引發 PropertyChanged 事件,這會導致處理程式的屬性對應程式呼叫 UpdatePosition 方法。 方法 UpdatePosition 應該不會對大部分的屬性變更執行任何動作。 否則,視訊位置的每個變更都會移至剛到達的相同位置。 為了避免這個意見反應迴圈,UpdatePosition只有在 屬性與 目前位置VideoView之間的差異Position大於一秒時,才會在物件上VideoView呼叫 Seek 方法。

iOS 和 Mac Catalyst

在 iOS 和 Mac Catalyst 上 AVPlayerItem.CurrentTime ,屬性會指出影片的目前位置。 類別MauiVideoPlayerPosition會在 方法中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));
            }
        }
        ...
    }
}

屬性每次 PositionUpdateStatus 方法設定時, Position 屬性都會引發 PropertyChanged 事件,這會導致處理程式的屬性對應程式呼叫 UpdatePosition 方法。 方法 UpdatePosition 應該不會對大部分的屬性變更執行任何動作。 否則,視訊位置的每個變更都會移至剛到達的相同位置。 為了避免這個意見反應迴圈,UpdatePosition只有在 屬性與 目前位置AVPlayer之間的差異Position大於一秒時,才會在物件上AVPlayer呼叫 Seek 方法。

Windows

在 Windows 上 MediaPlayerElement.MedaPlayer.Position ,屬性會指出視訊的目前位置。 類別MauiVideoPlayerPosition會在 方法中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;
                }
            }
        }
        ...
    }
}

屬性每次 PositionUpdateStatus 方法設定時, 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;
        }
        ...
    }
}

SetTimeToEndPosition 屬性的屬性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;
                }
            };
        }
    }
}

屬性的屬性變更事件處理程式會將 DurationSlider 屬性設定MaximumTotalSeconds 值的 屬性TimeSpan。 同樣地,屬性的屬性變更事件處理程序 Position 會設定 ValueSlider屬性。 這是追蹤 位置PositionSlider的機制Slider

PositionSlider只會在一個案例中從基礎Slider更新 ,也就是使用者操作 Slider 時,表示視訊應進階或反轉為新位置。 在建構函式的PropertyChangedPositionSlider處理程式中偵測到這個值。 這個事件處理程式會檢查屬性中的 Value 變更,如果與 屬性不同 Position ,則會 PositionValue 屬性設定 屬性。

註冊處理程式

自定義控件及其處理程式必須先向應用程式註冊,才能取用。 這應該發生在 CreateMauiApp 應用程式項目中 類別的 MauiProgram 方法中,這是應用程式的跨平臺進入點:

using VideoDemos.Controls;
using VideoDemos.Handlers;

namespace VideoDemos;

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

        return builder.Build();
    }
}

處理程式會向 ConfigureMauiHandlersAddHandler 方法註冊。 方法的第一個自變數 AddHandler 是跨平臺控件類型,第二個自變數是其處理程序類型。

取用跨平臺控制件

向您的應用程式註冊處理程序之後,就可以取用跨平臺控制件。

播放 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 的詳細資訊,請參閱 媒體選擇器。 對於每個平台而言,因為檔案位於裝置上,而且不需下載,所以當設定好影片來源之後,就會幾乎立即開始播放影片。 在每個平臺上,如果傳輸控件未使用,但可以點選視訊來還原,傳輸控件就會淡出。

設定影片控制件

您可以將 屬性設定 AutoPlayfalse,以防止影片自動啟動:

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

您可以將 屬性設定 AreTransportControlsEnabledfalse,以隱藏傳輸控制項:

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

如果您將 和 AreTransportControlsEnabled 設定AutoPlayfalse,則影片不會開始播放,而且無法開始播放。 在此案例中,您必須從程序代碼後置檔案呼叫 Play 方法,或建立您自己的傳輸控件。

此外,您可以藉由將 屬性設定為 ,將影片設定 IsLooping 為迴圈 true:

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

如果您將 屬性設定 IsLoopingtrue ,這可確保 Video 控件在到達結束之後,會自動將視訊位置設定為開始。

使用自定義傳輸控制件

下列 XAML 範例顯示播放、暫停和停止影片的自訂傳輸控制項:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:VideoDemos.Controls"
             x:Class="VideoDemos.Views.CustomTransportPage"
             Unloaded="OnContentPageUnloaded"
             Title="Custom transport controls">
    <Grid x:DataType="controls:Video"
          RowDefinitions="*,Auto">
        <controls:Video x:Name="video"
                        AutoPlay="False"
                        AreTransportControlsEnabled="False"
                        Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
        <ActivityIndicator Color="Gray"
                           IsVisible="False">
            <ActivityIndicator.Triggers>
                <DataTrigger TargetType="ActivityIndicator"
                             Binding="{Binding Source={x:Reference video},
                                               Path=Status}"
                             Value="{x:Static controls:VideoStatus.NotReady}">
                    <Setter Property="IsVisible"
                            Value="True" />
                    <Setter Property="IsRunning"
                            Value="True" />
                </DataTrigger>
            </ActivityIndicator.Triggers>
        </ActivityIndicator>
        <Grid Grid.Row="1"
              Margin="0,10"
              ColumnDefinitions="0.5*,0.5*"
              BindingContext="{x:Reference video}">
            <Button Text="&#x25B6;&#xFE0F; Play"
                    HorizontalOptions="Center"
                    Clicked="OnPlayPauseButtonClicked">
                <Button.Triggers>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.Playing}">
                        <Setter Property="Text"
                                Value="&#x23F8; Pause" />
                    </DataTrigger>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.NotReady}">
                        <Setter Property="IsEnabled"
                                Value="False" />
                    </DataTrigger>
                </Button.Triggers>
            </Button>
            <Button Grid.Column="1"
                    Text="&#x23F9; Stop"
                    HorizontalOptions="Center"
                    Clicked="OnStopButtonClicked">
                <Button.Triggers>
                    <DataTrigger TargetType="Button"
                                 Binding="{Binding Status}"
                                 Value="{x:Static controls:VideoStatus.NotReady}">
                        <Setter Property="IsEnabled"
                                Value="False" />
                    </DataTrigger>
                </Button.Triggers>
            </Button>
        </Grid>
    </Grid>
</ContentPage>

在這裡範例中,控制項會將 Video AreTransportControlsEnabled 屬性設定為 false ,並定義 Button 播放和暫停視訊的 ,以及 Button 停止視訊播放的 。 按鈕外觀是使用 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對象的 屬性會系結至 PositionPositionSlider屬性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 變數,且列舉定義下列值:

下列範例顯示設定 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.DisconnectPolicyManual 時,您必須從應用程式生命週期中的適當位置自行叫用處理程式 DisconnectHandler 的實作。 這可以藉由叫 video.Handler?.DisconnectHandler();用 來達成。

此外,還有一個 DisconnectHandlers 擴充方法可將處理程式與指定的 IView中斷連接:

video.DisconnectHandlers();

中斷連線時, DisconnectHandlers 方法會傳播到控件樹狀結構,直到它完成或到達已設定手動原則的控件為止。