自訂視訊效果

本文說明如何建立一個實作 IBasicVideoEffect 介面的 Windows 執行階段 元件,以為影片串流創造自訂特效。 你可以用 MediaCapture 和 MediaComposition 使用自訂效果。

Note

介面IBasicVideoEffect是命名空間中的 Windows.Media.Effects Windows 執行階段 API,介面成員也是你在 UWP 中實作的。 然而,WinUI 3 桌面應用程式沒有 UWP 專案所使用的 Windows 執行階段 元件專案範本。 相反地,你必須使用 C#/WinRT 類別函式庫來撰寫這個效果,並且必須明確註冊該元件以啟用 Windows 執行階段,正如本文所述。

為你的影片效果新增一個 Windows 執行階段 元件

WinUI 3 桌面應用程式使用 C#/WinRT 來撰寫 Windows 執行階段 元件,而非僅支援 UWP 的 Windows 執行階段 元件專案範本。

  1. 在 方案總管 中右鍵點擊你的解決方案,選擇新增>Project。

  2. 選擇 類別庫 專案範本。 專案名稱 為 VideoEffectComponent。

  3. 在 VideoEffectComponent.csproj 中,設定目標框架以符合你的 WinUI 3 應用程式,並將專案標記為 Windows 執行階段 元件:

    <PropertyGroup>
        <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
        <CsWinRTComponent>true</CsWinRTComponent>
    </PropertyGroup>
    
  4. 在 VideoEffectComponent 專案中安裝最新的 Microsoft.Windows.CsWinRT NuGet 套件。

  5. 從你的主 WinUI 3 應用程式中,將專案參考加入這個元件專案。

  6. 將預設類別檔案重新命名為 ExampleVideoEffect.cs。

想了解更多關於以此方式撰寫元件的資訊,請參閱 「攻略—建立 C#/WinRT 元件」。

註冊效果元件以便啟用

VideoEffectDefinition透過其 Windows 執行階段 可啟用的類別 ID(你傳遞給typeof(...).FullName的完整型別名稱)來啟動你的效應。 除非你註冊了該類別 ID,否則啟動會在執行時失敗,並出現「類別未註冊」例外,即使程式碼會編譯。 如何註冊該類別,取決於你的應用程式是否已封裝。

已封裝的應用程式

將一個 <Extensions> 項目新增至 Package.appxmanifest 中,以宣告該效果是由 WinRT.Host.dll 裝載的同處理序可啟用類別;WinRT.Host.dll 也是 C#/WinRT 會加入至您的建置輸出的裝載組件:

<Extensions>
    <Extension Category="windows.activatableClass.inProcessServer">
        <InProcessServer>
            <Path>WinRT.Host.dll</Path>
            <ActivatableClass
                ActivatableClassId="VideoEffectComponent.ExampleVideoEffect"
                ThreadingModel="both" />
        </InProcessServer>
    </Extension>
</Extensions>

Note

ActivatableClassId 必須完全符合你傳給 VideoEffectDefinition的命名空間限定類別名稱。

未封裝應用程式

未封裝的應用程式沒有 Package.appxmanifest,所以你會把可啟用的類別註冊在應用程式清單檔案中。 新增一個名為 YourApp.exe.manifest 的新文字檔到你的應用程式專案中,將其 內容屬性設 為 True ,以便複製到輸出目錄,並以以下格式新增相同的類別註冊:

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
    <assemblyIdentity version="1.0.0.0" name="YourApp"/>
    <file name="WinRT.Host.dll">
        <activatableClass
            name="VideoEffectComponent.ExampleVideoEffect"
            threadingModel="both"
            xmlns="urn:schemas-microsoft-com:winrt.v1" />
    </file>
</assembly>

欲了解更多關於 C#/WinRT 元件的託管與註冊資訊,請參閱 C#/WinRT GitHub 倉庫中的受管理元件託管。

使用軟體處理實作 IBasicVideoEffect 介面

你的影片效果必須實作 IBasicVideoEffect 介面的所有方法與屬性。 本節展示軟體處理實作。

類別定義與命名空間

using System.Collections.Generic;
using System.Runtime.InteropServices;
using Windows.Foundation.Collections;
using Windows.Graphics.Imaging;
using Windows.Media;
using Windows.Media.Effects;
using Windows.Media.MediaProperties;

namespace VideoEffectComponent
{
    public sealed class ExampleVideoEffect : IBasicVideoEffect
    {
        private VideoEncodingProperties _encodingProperties;
        private IPropertySet _configuration;
        private double _fadeValue = 0.5;

        // The following members implement the IBasicVideoEffect and
        // IMediaExtension interfaces. Each member is explained in its own
        // section later in this article.
        public void SetEncodingProperties(
            VideoEncodingProperties encodingProperties,
            Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice device)
        {
            _encodingProperties = encodingProperties;
        }

        public void SetProperties(IPropertySet configuration)
        {
            _configuration = configuration;

            if (configuration != null &&
                configuration.TryGetValue("FadeValue", out object value))
            {
                _fadeValue = (double)value;
            }
        }

        public void ProcessFrame(ProcessVideoFrameContext context)
        {
            // See ProcessFrame method — software processing later in
            // this article for the full implementation.
        }

        public void DiscardQueuedFrames()
        {
            // Reset any cached frame data
        }

        public void Close(MediaEffectClosedReason reason)
        {
            // Clean up resources
        }

        public bool IsReadOnly => false;

        public bool TimeIndependent => true;

        public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties
        {
            get
            {
                var properties = new List<VideoEncodingProperties>();
                properties.Add(new VideoEncodingProperties
                {
                    Subtype = "ARGB32"
                });
                return properties;
            }
        }

        public MediaMemoryTypes SupportedMemoryTypes => MediaMemoryTypes.Cpu;
    }
}

Note

ExampleVideoEffect 類別必須在此處顯示的 VideoEffectComponent 命名空間內宣告,因為本文後續的 typeof(VideoEffectComponent.ExampleVideoEffect).FullName 呼叫以及資訊清單註冊中的 ActivatableClassId 值,都取決於這個完全限定的命名空間名稱。 以下各節將逐一詳細說明每個介面成員;此處顯示的 ProcessFrame 方法只是預留位置,會由 ProcessFrame 方法——軟體處理 中的完整像素處理實作取代。

Close 方法

系統在效果關閉時呼叫 關閉 。 用這個方法處理你創造的任何資源。

public void Close(MediaEffectClosedReason reason)
{
    // Clean up resources
}

DiscardQueuedFrames 方法

系統會在您的效果需要重設時呼叫 DiscardQueuedFrames。 利用此方法處理先前快取的幀。

public void DiscardQueuedFrames()
{
    // Reset any cached frame data
}

IsReadOnly 屬性

IsReadOnly 屬性告訴系統你的效果是否寫入輸出。 如果你的效果只分析影格,設定為 true 系統會將影格從輸入複製到輸出。

public bool IsReadOnly
{
    get => false;
}

Tip

當 IsReadOnly 時 true,系統會在呼叫 ProcessFrame 前將輸入幀複製到輸出幀。 你仍然可以在 ProcessFrame 中寫入輸出影格。

SetEncodingProperties 方法

系統會呼叫 SetEncodingProperties,將影片串流的編碼屬性告知你的效果。 此方法同時也參考了硬體渲染的 Direct3D 裝置。

private Windows.Media.MediaProperties.VideoEncodingProperties _encodingProperties;

public void SetEncodingProperties(
    VideoEncodingProperties encodingProperties,
    Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice device)
{
    _encodingProperties = encodingProperties;
}

SupportedEncodingProperties 屬性

系統會檢查 SupportedEncodingProperties 以判斷你的效果支援哪些編碼屬性。

public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties
{
    get
    {
        var properties = new List<VideoEncodingProperties>();
        properties.Add(new VideoEncodingProperties
        {
            Subtype = "ARGB32"
        });
        return properties;
    }
}

Note

如果你回傳的物件清單 VideoEncodingProperties 是空的,系統會預設使用 ARGB32 編碼。

支援的記憶體類型屬性

SupportedMemoryTypes 屬性決定效果存取的是軟體記憶體中的影片影格,還是 GPU 記憶體中的影片影格。

public MediaMemoryTypes SupportedMemoryTypes
{
    get => MediaMemoryTypes.Cpu;
}

如果你回傳 MediaMemoryTypes.Cpu,系統會將影格以 SoftwareBitmap 物件傳遞。 如果你返回 MediaMemoryTypes.Gpu,系統會將幀以 IDirect3DSurface 物件傳遞。

TimeIndependent 屬性

如果你的效果不需要均勻的時序,就將 TimeIndependent 設為 true 。 這讓系統能夠優化效能。

public bool TimeIndependent
{
    get => true;
}

SetProperties 方法

SetProperties 方法允許呼叫應用程式將設定參數傳給你的效果器。

private double _fadeValue = 0.5;
private Windows.Foundation.Collections.IPropertySet _configuration;

public void SetProperties(IPropertySet configuration)
{
    _configuration = configuration;

    if (configuration != null &&
        configuration.TryGetValue("FadeValue", out object value))
    {
        _fadeValue = (double)value;
    }
}

ProcessFrame 方法 — 軟體處理

ProcessFrame 方法是您的效果修改影像資料的地方。 此方法每個影格呼叫一次,並接收一個 ProcessVideoFrameContext 物件,其中包含輸入和輸出 VideoFrame 物件。

若要存取 SoftwareBitmap 的原始像素資料,請使用 COM 互通操作。 在你的效果命名空間中加入以下介面定義:

[ComImport]
[System.Runtime.InteropServices.Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Note

此技術存取原生且未受管理的影像緩衝區。 你必須設定專案允許不安全的程式碼。 在專案屬性中,選擇 建置 標籤並啟用 允許不安全的程式碼。

以下範例會依照設定的淡化值,將影格中的每個像素調暗:

public unsafe void ProcessFrame(ProcessVideoFrameContext context)
{
    using (BitmapBuffer inputBuffer = context.InputFrame
        .SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
    using (BitmapBuffer outputBuffer = context.OutputFrame
        .SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
    {
        using (var inputRef = inputBuffer.CreateReference())
        using (var outputRef = outputBuffer.CreateReference())
        {
            byte* inputBytes;
            uint inputCapacity;
            ((IMemoryBufferByteAccess)inputRef)
                .GetBuffer(out inputBytes, out inputCapacity);

            byte* outputBytes;
            uint outputCapacity;
            ((IMemoryBufferByteAccess)outputRef)
                .GetBuffer(out outputBytes, out outputCapacity);

            var inputPlane =
                inputBuffer.GetPlaneDescription(0);

            for (int i = 0;
                 i < inputPlane.Height;
                 i++)
            {
                for (int j = 0;
                     j < inputPlane.Width;
                     j++)
                {
                    int offset = inputPlane.StartIndex
                        + inputPlane.Stride * i
                        + 4 * j;

                    // Apply fade to B, G, R channels
                    // (skip alpha at offset+3)
                    outputBytes[offset + 0] = (byte)(
                        inputBytes[offset + 0] * _fadeValue);
                    outputBytes[offset + 1] = (byte)(
                        inputBytes[offset + 1] * _fadeValue);
                    outputBytes[offset + 2] = (byte)(
                        inputBytes[offset + 2] * _fadeValue);
                    outputBytes[offset + 3] =
                        inputBytes[offset + 3]; // alpha
                }
            }
        }
    }
}

使用Win2D進行硬體處理

基於 GPU 的處理,請使用 Win2D 代替軟體點陣圖操作。 使用硬體處理時:

  1. 將 Microsoft.Graphics.Win2D NuGet 套件新增至你的效果專案。
  2. 從 SupportedMemoryTypes 傳回 MediaMemoryTypes.Gpu。
  3. 從 SetEncodingProperties 儲存 Direct3D 裝置參考。
  4. 在 ProcessFrame中,從 Direct3D 裝置建立 a CanvasDevice ,並對輸出畫面 Direct3DSurface進行 Win2D 繪圖操作。

Note

對於 WinUI 3 專案,請使用 Microsoft.Graphics.Win2D 套件,而不是較舊的 Win2D.uwp 套件。

將效果新增至影片串流

將影片效果加入 MediaCapture 影片串流:

var effectDefinition = new VideoEffectDefinition(
    typeof(VideoEffectComponent.ExampleVideoEffect).FullName);

await _mediaCapture.AddVideoEffectAsync(
    effectDefinition,
    MediaStreamType.VideoPreview);

若要傳遞組態屬性:

var properties = new PropertySet();
properties["FadeValue"] = 0.7;

var effectDefinition = new VideoEffectDefinition(
    typeof(VideoEffectComponent.ExampleVideoEffect).FullName,
    properties);

將效果加入媒體構圖

在 MediaComposition 中將視訊效果新增至片段:

var effectDefinition = new VideoEffectDefinition(
    typeof(VideoEffectComponent.ExampleVideoEffect).FullName);

mediaClip.VideoEffectDefinitions.Add(effectDefinition);