如何撰寫EVR演示者

[此頁面所述的元件, 增強的視訊轉譯器是舊版功能。 它已被透過 MediaPlayerIMFMediaEngine 元件公開的簡單視頻轉譯器(SVR)所取代。 若要播放視訊內容,您應該將數據傳送到其中一個元件,並允許它們具現化新的視訊轉譯器。 這些元件已針對 Windows 10 和 Windows 11 優化。 Microsoft 強烈建議新程式代碼盡可能使用 MediaPlayer 或較低層級 的 IMFMediaEngine API 在 Windows 中播放視訊媒體,而不是 EVR。 Microsoft 建議將使用舊版 API 的現有程式代碼重寫為盡可能使用新的 API。]

本文說明如何為增強的視訊轉譯器 (EVR) 撰寫自定義演示者。 自定義演示者可以搭配 DirectShow 和 Media Foundation 使用;這兩種技術的介面和物件模型都相同,不過作業的確切順序可能會有所不同。

本主題中的範例程式代碼是改編自 Windows SDK 中提供的 EVRPresenter 範例

本主題包含下列幾節:

必要條件

撰寫自定義演示者之前,您應該先熟悉下列技術:

  • 增強的視訊轉譯器。 請參閱 增強的視訊轉譯器
  • Direct3D 圖形。 您不需要瞭解 3D 圖形來撰寫演示者,但您必須知道如何建立 Direct3D 裝置及管理 Direct3D 表面。 如果您不熟悉 Direct3D,請閱讀 DirectX Graphics SDK 檔中的一節。
  • DirectShow 篩選圖形或媒體基礎管線,視您的應用程式將用來轉譯視訊的技術而定。
  • 媒體基礎轉換。 EVR 混音器是媒體基礎轉換,演示者會直接在混音器上呼叫方法。
  • 實作 COM 物件。 演示者是一個進程、自由線程的 COM 物件。

演示者物件模型

本節包含演示者物件模型和介面的概觀。

EVR 內的數據流

EVR 使用兩個外掛程式元件來轉譯影片:混音器和演示者。 混音器會混合視訊串流,並視需要取消交錯視訊。 演示者會在繪製每個畫面時,將視訊繪製到顯示器上並排程。 應用程式可以使用自定義實作來取代其中一個物件。

EVR 有一或多個輸入數據流,而混音器具有對應的輸入數據流數目。 數據流 0 一律是 參考數據流。 其他數據流是 子數據流,混音器 Alpha 混合到參考數據流。 參考數據流會決定複合視訊的主要幀速率。 針對每個參考框架,混音器會從每個子數據流取得最新的畫面,將Alpha混合到參考框架,並輸出單一複合框架。 如有需要,混音器也會執行從 YUV 到 RGB 的除交和色彩轉換。 不論輸入數據流數目或視訊格式為何,EVR 一律會將混音器插入影片管線中。 下圖說明此程式。

diagram showing the reference stream and substream pointing to the mixer, which points to the presenter, which points to the display

演示者會執行下列工作:

  • 在混音器上設定輸出格式。 串流開始之前,演示者會在混音器的輸出數據流上設定媒體類型。 此媒體類型會定義複合影像的格式。
  • 建立 Direct3D 裝置。
  • 配置 Direct3D 介面。 混音器會將複合框架點選到這些表面。
  • 從混音器取得輸出。
  • 排程顯示框架的時間。 EVR 提供簡報時鐘,而演示者會根據這個時鐘排程畫面。
  • 使用 Direct3D 呈現每個畫面。
  • 執行框架逐步執行和清除。

演示者狀態

演示者隨時處於下列其中一種狀態:

  • 已啟動。 EVR 的簡報時鐘正在執行。 演示者會在簡報送達時排程視訊畫面。
  • 已暫停。 簡報時鐘已暫停。 演示者不會呈現任何新的樣本,但會維護其排程樣本佇列。 如果收到新的範例,演示者會將它們新增至佇列。
  • 已停止。 簡報時鐘已停止。 演示者會捨棄任何已排程的樣本。
  • 關機。 演示者會釋放任何與串流相關的資源,例如 Direct3D 介面。 這是演示者的初始狀態,以及演示者終結之前的最後狀態。

在本主題的範例程式代碼中,演示者狀態是由 列舉表示:

enum RENDER_STATE
{
    RENDER_STATE_STARTED = 1,
    RENDER_STATE_STOPPED,
    RENDER_STATE_PAUSED,
    RENDER_STATE_SHUTDOWN,  // Initial state.
};

當演示者處於關機狀態時,某些作業無效。 範例程式代碼會藉由呼叫協助程式方法來檢查此狀態:

    HRESULT CheckShutdown() const
    {
        if (m_RenderState == RENDER_STATE_SHUTDOWN)
        {
            return MF_E_SHUTDOWN;
        }
        else
        {
            return S_OK;
        }
    }

演示者介面

實作下列介面需要演示者:

介面 描述
IMFClockStateSink 當EVR的時鐘變更狀態時,通知演示者。 請參閱 實作IMFClockStateSink
IMFGetService 為管線中的應用程式和其他元件提供從演示者取得介面的方法。
IMFTopologyServiceLookupClient 可讓演示者從EVR或混音器取得介面。 請參閱 實作 IMFTopologyServiceLookupClient
IMFVideoDeviceID 確保演示者和混音器使用相容的技術。 請參閱 實作IMFVideoDeviceID
IMFVideoPresenter 處理來自EVR的訊息。 請參閱 實作IMFVideoPresenter

 

下列介面是選擇性的:

介面 描述
IEVRTrustedVideoPlugin 可讓演示者使用受保護的媒體。 如果您的演示者是設計成在受保護媒體路徑中運作的受信任元件,請實作這個介面。
IMFRateSupport 報告演示者支援的播放速率範圍。 請參閱 實作IMFRateSupport
IMFVideoPositionMapper 地圖 輸出視訊畫面上的座標,以輸入視訊畫面上的座標。
IQualProp 報告效能資訊。 EVR 會使用這項資訊進行品質控制管理。 此介面記載於 DirectShow SDK 中。

 

您也可以提供介面讓應用程式與演示者通訊。 標準演示者會針對此目的實作 IMFVideoDisplayControl 介面。 您可以實作此介面或定義您自己的介面。 應用程式會在 EVR 上呼叫 IMFGetService::GetService ,從演示者取得介面。 當服務 GUID MR_VIDEO_RENDER_SERVICE時,EVR 會將 GetService 要求傳遞給演示者。

實作 IMFVideoDeviceID

IMFVideoDeviceID 介面包含一個方法 GetDeviceID,它會傳回裝置 GUID。 裝置 GUID 可確保演示者和混音器使用相容的技術。 如果裝置 GUID 不相符,EVR 就無法初始化。

標準混音器和演示者都使用 Direct3D 9,裝置 GUID 等於 IID_IDirect3DDevice9。 如果您想要將自定義演示者與標準混音器搭配使用,則演示者的裝置 GUID 必須 IID_IDirect3DDevice9。 如果您取代這兩個元件,您可以定義新的裝置 GUID。 本文的其餘部分假設演示者使用 Direct3D 9。 以下是 GetDeviceID 的標準實作

HRESULT EVRCustomPresenter::GetDeviceID(IID* pDeviceID)
{
    if (pDeviceID == NULL)
    {
        return E_POINTER;
    }

    *pDeviceID = __uuidof(IDirect3DDevice9);
    return S_OK;
}

即使演示者關閉,方法也應該成功。

實作 IMFTopologyServiceLookupClient

IMFTopologyServiceLookupClient 介面可讓演示者從 EVR 和混音器取得介面指標,如下所示:

  1. 當 EVR 初始化演示者時,它會呼叫演示者的 IMFTopologyServiceLookupClient::InitServicePointers 方法。 自變數是 EVR 之 IMFTopologyServiceLookup 介面的指標。
  2. 演示者會呼叫 IMFTopologyServiceLookup::LookupService ,從 EVR 或混音器取得介面指標。

LookupService 方法類似於 IMFGetService::GetService 方法。 這兩種方法都會接受服務 GUID 和介面標識碼 (IID) 作為輸入,但 LookupService 會傳回介面指標的陣列,而 GetService 會傳回單一指標。 不過,在實務上,您一律可以將數位大小設定為 1。 查詢的物件取決於服務 GUID:

  • 如果服務 GUID 是 MR_VIDEO_RENDER_SERVICE,則會查詢 EVR。
  • 如果服務 GUID 是 MR_VIDEO_MIXER_SERVICE,則會查詢混音器。

在您的 InitServicePointers 實作中,從 EVR 取得下列介面:

EVR 介面 描述
IMediaEventSink 提供讓演示者將訊息傳送至 EVR 的方式。 此介面定義於 DirectShow SDK 中,因此訊息會遵循 DirectShow 事件的模式,而不是 Media Foundation 事件。
IMFClock 表示 EVR 的時鐘。 演示者會使用此介面來排程簡報的範例。 EVR 可以在沒有時鐘的情況下執行,因此此介面可能無法使用。 如果沒有,請忽略 LookupService 的錯誤碼
時鐘也會實作 IMFTimer 介面。 在媒體基礎管線中,時鐘會實作 IMFPresentationClock 介面。 它不會在 DirectShow 中實作這個介面。

 

從混音器取得下列介面:

混合器介面 描述
IMFTransform 可讓演示者與混音器通訊。
IMFVideoDeviceID 可讓演示者驗證混音器的裝置 GUID。

 

下列程式代碼會實作 InitServicePointers 方法:

HRESULT EVRCustomPresenter::InitServicePointers(
    IMFTopologyServiceLookup *pLookup
    )
{
    if (pLookup == NULL)
    {
        return E_POINTER;
    }

    HRESULT             hr = S_OK;
    DWORD               dwObjectCount = 0;

    EnterCriticalSection(&m_ObjectLock);

    // Do not allow initializing when playing or paused.
    if (IsActive())
    {
        hr = MF_E_INVALIDREQUEST;
        goto done;
    }

    SafeRelease(&m_pClock);
    SafeRelease(&m_pMixer);
    SafeRelease(&m_pMediaEventSink);

    // Ask for the clock. Optional, because the EVR might not have a clock.
    dwObjectCount = 1;

    (void)pLookup->LookupService(
        MF_SERVICE_LOOKUP_GLOBAL,   // Not used.
        0,                          // Reserved.
        MR_VIDEO_RENDER_SERVICE,    // Service to look up.
        IID_PPV_ARGS(&m_pClock),    // Interface to retrieve.
        &dwObjectCount              // Number of elements retrieved.
        );

    // Ask for the mixer. (Required.)
    dwObjectCount = 1;

    hr = pLookup->LookupService(
        MF_SERVICE_LOOKUP_GLOBAL, 0,
        MR_VIDEO_MIXER_SERVICE, IID_PPV_ARGS(&m_pMixer), &dwObjectCount
        );

    if (FAILED(hr))
    {
        goto done;
    }

    // Make sure that we can work with this mixer.
    hr = ConfigureMixer(m_pMixer);
    if (FAILED(hr))
    {
        goto done;
    }

    // Ask for the EVR's event-sink interface. (Required.)
    dwObjectCount = 1;

    hr = pLookup->LookupService(
        MF_SERVICE_LOOKUP_GLOBAL, 0,
        MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_pMediaEventSink),
        &dwObjectCount
        );

    if (FAILED(hr))
    {
        goto done;
    }

    // Successfully initialized. Set the state to "stopped."
    m_RenderState = RENDER_STATE_STOPPED;

done:
    LeaveCriticalSection(&m_ObjectLock);
    return hr;
}

當從 LookupService 取得的介面指標不再有效時,EVR 會呼叫 IMFTopologyServiceLookupClient::ReleaseServicePointers 在此方法中,釋放所有介面指標,並將演示者狀態設定為關閉:

HRESULT EVRCustomPresenter::ReleaseServicePointers()
{
    // Enter the shut-down state.
    EnterCriticalSection(&m_ObjectLock);

    m_RenderState = RENDER_STATE_SHUTDOWN;

    LeaveCriticalSection(&m_ObjectLock);

    // Flush any samples that were scheduled.
    Flush();

    // Clear the media type and release related resources.
    SetMediaType(NULL);

    // Release all services that were acquired from InitServicePointers.
    SafeRelease(&m_pClock);
    SafeRelease(&m_pMixer);
    SafeRelease(&m_pMediaEventSink);

    return S_OK;
}

EVR 會基於各種原因呼叫 ReleaseServicePointers ,包括:

  • 中斷連接或重新連接針腳(DirectShow),或新增或移除數據流接收(媒體基礎)。
  • 變更格式。
  • 設定新的時鐘。
  • EVR 的最終關機。

在演示者的存留期間,EVR 可能會多次呼叫 InitServicePointersReleaseServicePointers

實作IMFVideoPresenter

IMFVideoPresenter 介面會繼承 IMFClockStateSink,並新增兩種方法:

方法 描述
GetCurrentMediaType 傳回復合視訊畫面的媒體類型。
ProcessMessage 指示演示者執行各種動作。

 

GetCurrentMediaType 方法會傳回演示者的媒體類型。 (如需設定媒體類型的詳細資訊,請參閱 交涉格式。)媒體類型會以 IMFVideoMediaType 介面指標的形式傳回。 下列範例假設演示者會將媒體類型儲存為 IMFMediaType 指標。 若要從媒體類型取得 IMFVideoMediaType 介面,請呼叫 QueryInterface

HRESULT EVRCustomPresenter::GetCurrentMediaType(
    IMFVideoMediaType** ppMediaType
    )
{
    HRESULT hr = S_OK;

    if (ppMediaType == NULL)
    {
        return E_POINTER;
    }

    *ppMediaType = NULL;

    EnterCriticalSection(&m_ObjectLock);

    hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    if (m_pMediaType == NULL)
    {
        hr = MF_E_NOT_INITIALIZED;
        goto done;
    }

    hr = m_pMediaType->QueryInterface(IID_PPV_ARGS(ppMediaType));

done:
    LeaveCriticalSection(&m_ObjectLock);
    return hr;
}

ProcessMessage 方法是 EVR 與演示者通訊的主要機制。 定義下列訊息。 本主題其餘部分會提供實作每個訊息的詳細數據。

訊息 描述
MFVP_MESSAGE_INVALIDATEMEDIATYPE 混音器的輸出媒體類型無效。 演示者應該與混音器交涉新的媒體類型。 請參閱 交涉格式
MFVP_MESSAGE_BEGINSTREAMING 串流已開始。 此訊息不需要採取任何特定動作,但您可以使用它來配置資源。
MFVP_MESSAGE_ENDSTREAMING 串流已結束。 釋放您配置的任何資源,以回應 MFVP_MESSAGE_BEGINSTREAMING 訊息。
MFVP_MESSAGE_PROCESSINPUTNOTIFY 混音器已收到新的輸入範例,而且可能會產生新的輸出框架。 演示者應該在混音器上呼叫 IMFTransform::P rocessOutput 請參閱 處理輸出
MFVP_MESSAGE_ENDOFSTREAM 簡報已結束。 請參閱 數據流結尾。
MFVP_MESSAGE_FLUSH EVR 正在排清其轉譯管線中的數據。 演示者應該捨棄排程用於簡報的任何視訊畫面。
MFVP_MESSAGE_STEP 要求演示者向前推進 N 框架。 演示者應該捨棄下一個 N-1 畫面,並顯示第 N 個畫面。 請參閱 框架逐步執行
MFVP_MESSAGE_CANCELSTEP 取消框架逐步執行。

 

實作 IMFClockStateSink

演示者必須實作 IMFClockStateSink 介面,作為其 IMFVideoPresenter 實作的一部分,該介面繼承了 IMFClockStateSink。 每當 EVR 的時鐘變更狀態時,EVR 會使用此介面通知演示者。 如需時鍾狀態的詳細資訊,請參閱 簡報時鐘

以下是在此介面中實作方法的一些指導方針。 如果演示者已關閉,則所有方法都應該失敗。

方法 描述
OnClockStart
  1. 將演示者狀態設定為已啟動。
  2. 如果 llClockStartOffsetPRESENTATION_CURRENT_POSITION,請排清演示者的範例佇列。 (這相當於接收 MFVP_MESSAGE_FLUSH訊息。)
  3. 如果先前的框架步驟要求仍在擱置中,請處理要求(請參閱 框架逐步執行)。 否則,請嘗試處理混音器的輸出(請參閱 處理輸出
OnClockStop
  1. 將演示者狀態設定為已停止。
  2. 排清演示者的樣本佇列。
  3. 取消任何擱置的框架步驟作業。
OnClockPause 將演示者狀態設定為已暫停。
OnClockRestart 將與 OnClockStart 相同,但不會排清範例的佇列。
OnClockSetRate
  1. 如果速率從零變更為非零,請取消框架逐步執行。
  2. 儲存新的時鐘速率。 時鐘速率會影響樣本呈現的時間。 如需詳細資訊,請參閱 排程範例

 

實作IMFRateSupport

若要支援 1× 速度以外的播放速率,演示者必須實 作 IMFRateSupport 介面。 以下是在此介面中實作方法的一些指導方針。 當演示者關閉之後,所有方法都應該失敗。 如需此介面的詳細資訊,請參閱 速率控制

Description
GetSlowestRate 傳回零表示沒有最小播放速率。
GetFastestRate 對於非精簡播放,播放速率不應超過監視器的重新整理速率: 最大速率 = 重新整理速率 (Hz)/ 視訊幀速率 (fps)。 視訊幀速率是在演示者的媒體類型中指定。
針對精簡播放,播放速率未系結;會傳回值 FLT_MAX。 在實務上,來源和譯碼器將是精簡播放期間的限制因素。
針對反向播放,傳回最大速率的負數。
IsRateSupported 如果 flRate絕對值超過演示者的播放速率上限,則傳回MF_E_UNSUPPORTED_RATE。 計算 GetFastestRate 所述的最大播放速率。

 

下列範例示範如何實 作 GetFastestRate 方法:

float EVRCustomPresenter::GetMaxRate(BOOL bThin)
{
    // Non-thinned:
    // If we have a valid frame rate and a monitor refresh rate, the maximum
    // playback rate is equal to the refresh rate. Otherwise, the maximum rate
    // is unbounded (FLT_MAX).

    // Thinned: The maximum rate is unbounded.

    float   fMaxRate = FLT_MAX;
    MFRatio fps = { 0, 0 };
    UINT    MonitorRateHz = 0;

    if (!bThin && (m_pMediaType != NULL))
    {
        GetFrameRate(m_pMediaType, &fps);
        MonitorRateHz = m_pD3DPresentEngine->RefreshRate();

        if (fps.Denominator && fps.Numerator && MonitorRateHz)
        {
            // Max Rate = Refresh Rate / Frame Rate
            fMaxRate = (float)MulDiv(
                MonitorRateHz, fps.Denominator, fps.Numerator);
        }
    }

    return fMaxRate;
}

上述範例會呼叫協助程式方法 GetMaxRate,以計算轉送播放速率上限:

下列範例示範如何實 作IsRateSupported 方法:

HRESULT EVRCustomPresenter::IsRateSupported(
    BOOL bThin,
    float fRate,
    float *pfNearestSupportedRate
    )
{
    EnterCriticalSection(&m_ObjectLock);

    float   fMaxRate = 0.0f;
    float   fNearestRate = fRate;  // If we support fRate, that is the nearest.

    HRESULT hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    // Find the maximum forward rate.
    // Note: We have no minimum rate (that is, we support anything down to 0).
    fMaxRate = GetMaxRate(bThin);

    if (fabsf(fRate) > fMaxRate)
    {
        // The (absolute) requested rate exceeds the maximum rate.
        hr = MF_E_UNSUPPORTED_RATE;

        // The nearest supported rate is fMaxRate.
        fNearestRate = fMaxRate;
        if (fRate < 0)
        {
            // Negative for reverse playback.
            fNearestRate = -fNearestRate;
        }
    }

    // Return the nearest supported rate.
    if (pfNearestSupportedRate != NULL)
    {
        *pfNearestSupportedRate = fNearestRate;
    }

done:
    LeaveCriticalSection(&m_ObjectLock);
    return hr;
}

將事件傳送至 EVR

演示者必須通知 EVR 各種事件。 若要這樣做,它會使用 EVR 的 IMediaEventSink 介面,當 EVR 呼叫演示者的 IMFTopologyServiceLookupClient::InitServicePointers 方法時取得。 (The IMediaEventSink 介面原本是 DirectShow 介面,但用於 DirectShow EVR 和媒體基礎。下列程式代碼示範如何將事件傳送至 EVR:

    // NotifyEvent: Send an event to the EVR through its IMediaEventSink interface.
    void NotifyEvent(long EventCode, LONG_PTR Param1, LONG_PTR Param2)
    {
        if (m_pMediaEventSink)
        {
            m_pMediaEventSink->Notify(EventCode, Param1, Param2);
        }
    }

下表列出演示者傳送的事件,以及事件參數。

事件 描述
EC_COMPLETE 演示者已完成在MFVP_MESSAGE_ENDOFSTREAM訊息之後轉譯所有畫面格。
  • Param1:指出作業狀態的 HRESULT。
  • Param2:未使用。
如需詳細資訊,請參閱 數據流結尾。
EC_DISPLAY_CHANGED Direct3D 裝置已變更。
  • Param1:未使用。
  • Param2:未使用。
如需詳細資訊,請參閱 管理 Direct3D 裝置
EC_ERRORABORT 發生錯誤,需要串流停止。
  • Param1HRESULT ,指出發生的錯誤。
  • Param2:未使用。
EC_PROCESSING_LATENCY 指定演示者轉譯每個畫面所花費的時間量。 (選用。)
  • Param1:常數 LONGLONG 值的指標,其中包含處理框架的時間量,以 100 奈秒為單位。
  • Param2:未使用。
如需詳細資訊,請參閱 處理輸出
EC_SAMPLE_LATENCY 指定轉譯範例中的目前延隔時間。 如果值為正數,則樣本會落後於排程。 如果值為負值,則樣本會提前排程。 (選用。)
  • Param1:常數 LONGLONG 值的指標,其中包含延遲時間,以 100 奈秒為單位。
  • Param2:未使用。
EC_SCRUB_TIME 如果播放速率為零,EC_STEP_COMPLETE之後立即傳送。 此事件包含所顯示框架的時間戳。
  • Param1:降低時間戳的 32 位。
  • Param2:時間戳的上層 32 位。
如需詳細資訊,請參閱 框架逐步執行
EC_STEP_COMPLETE 演示者已完成或取消框架步驟。
- Param1:未使用。
- Param2:未使用。
如需詳細資訊,請參閱 框架逐步執行
注意: 舊版的檔描述 Param1 不正確。 這個事件不會使用此參數。

 

交涉格式

每當演示者收到 來自EVR的 MFVP_MESSAGE_INVALIDATEMEDIATYPE訊息時,就必須在混音器上設定輸出格式,如下所示:

  1. 在混音器上呼叫 IMFTransform::GetOutputAvailableType 以取得可能的輸出類型。 此類型描述混音器可以在圖形裝置的輸入數據流和視訊處理功能時產生的格式。

  2. 檢查演示者是否可以使用此媒體類型做為其轉譯格式。 以下是要檢查的一些事項,雖然您的實作可能有自己的需求:

    • 影片必須解壓縮。
    • 影片必須只有漸進式畫面。 檢查MF_MT_INTERLACE_MODE屬性是否等於 MFVideoInterlace_Progressive
    • 格式必須與 Direct3D 裝置相容。

    如果無法接受此類型,請返回步驟 1 並取得混音器下一個建議的類型。

  3. 建立新的媒體類型,其為原始類型的複製品,然後變更下列屬性:

  4. 若要測試混音器是否接受修改的輸出類型,請使用 MFT_SET_TYPE_TEST_ONLY 旗標呼叫 IMFTransform::SetOutputType 如果混音器拒絕類型,請返回步驟 1 並取得下一個類型。

  5. 配置 Direct3D 介面的集區,如配置 Direct3D Surface 中所述。 混音器會在繪製複合式視訊畫面時使用這些表面。

  6. 呼叫 不含旗標的 SetOutputType ,在混音器上設定輸出類型。 如果第一次呼叫 SetOutputType 在步驟 4 中成功,方法應該會再次成功。

如果混音器的類型用盡,GetOutputAvailableType 方法會傳回MF_E_NO_MORE_TYPES 如果演示者找不到適合混音器的輸出類型,則無法轉譯數據流。 在此情況下,DirectShow 或 Media Foundation 可能會嘗試其他數據流格式。 因此,演示者可能會連續收到數 個MFVP_MESSAGE_INVALIDATEMEDIATYPE 訊息,直到找到有效的類型為止。

混音器會自動收件匣視訊,並考慮到來源和目的地的像素外觀比例(PAR)。 為了獲得最佳結果,表面寬度和高度和幾何光圈應該等於您希望視訊出現在顯示器上的實際大小。 下圖說明此程式。

diagram showing a composited fram leading to a direct3d surface, which leads to a window

下列程式代碼顯示程式的大綱。 某些步驟會放在協助程式函式中,其確切詳細數據將取決於您演示者的需求。

HRESULT EVRCustomPresenter::RenegotiateMediaType()
{
    HRESULT hr = S_OK;
    BOOL bFoundMediaType = FALSE;

    IMFMediaType *pMixerType = NULL;
    IMFMediaType *pOptimalType = NULL;
    IMFVideoMediaType *pVideoType = NULL;

    if (!m_pMixer)
    {
        return MF_E_INVALIDREQUEST;
    }

    // Loop through all of the mixer's proposed output types.
    DWORD iTypeIndex = 0;
    while (!bFoundMediaType && (hr != MF_E_NO_MORE_TYPES))
    {
        SafeRelease(&pMixerType);
        SafeRelease(&pOptimalType);

        // Step 1. Get the next media type supported by mixer.
        hr = m_pMixer->GetOutputAvailableType(0, iTypeIndex++, &pMixerType);
        if (FAILED(hr))
        {
            break;
        }

        // From now on, if anything in this loop fails, try the next type,
        // until we succeed or the mixer runs out of types.

        // Step 2. Check if we support this media type.
        if (SUCCEEDED(hr))
        {
            // Note: None of the modifications that we make later in CreateOptimalVideoType
            // will affect the suitability of the type, at least for us. (Possibly for the mixer.)
            hr = IsMediaTypeSupported(pMixerType);
        }

        // Step 3. Adjust the mixer's type to match our requirements.
        if (SUCCEEDED(hr))
        {
            hr = CreateOptimalVideoType(pMixerType, &pOptimalType);
        }

        // Step 4. Check if the mixer will accept this media type.
        if (SUCCEEDED(hr))
        {
            hr = m_pMixer->SetOutputType(0, pOptimalType, MFT_SET_TYPE_TEST_ONLY);
        }

        // Step 5. Try to set the media type on ourselves.
        if (SUCCEEDED(hr))
        {
            hr = SetMediaType(pOptimalType);
        }

        // Step 6. Set output media type on mixer.
        if (SUCCEEDED(hr))
        {
            hr = m_pMixer->SetOutputType(0, pOptimalType, 0);

            assert(SUCCEEDED(hr)); // This should succeed unless the MFT lied in the previous call.

            // If something went wrong, clear the media type.
            if (FAILED(hr))
            {
                SetMediaType(NULL);
            }
        }

        if (SUCCEEDED(hr))
        {
            bFoundMediaType = TRUE;
        }
    }

    SafeRelease(&pMixerType);
    SafeRelease(&pOptimalType);
    SafeRelease(&pVideoType);

    return hr;
}

如需視訊媒體類型的詳細資訊,請參閱 視訊媒體類型

管理 Direct3D 裝置

演示者會建立 Direct3D 裝置,並在串流期間處理任何裝置遺失。 演示者也會裝載 Direct3D 設備管理器,它提供讓其他元件使用相同的裝置的方式。 例如,混音器會使用 Direct3D 裝置來混合子流、取消交錯和執行色彩調整。 譯碼器可以使用 Direct3D 裝置進行視訊加速譯碼。 (如需影片加速的詳細資訊,請參閱 DirectX 影片加速 2.0.)

若要設定 Direct3D 裝置,請執行下列步驟:

  1. 呼叫 Direct3DCreate9Direct3DCreate9Ex 來建立 Direct3D 物件
  2. 呼叫 IDirect3D9::CreateDeviceIDirect3D9Ex::CreateDevice 來建立裝置。
  3. 呼叫 DXVA2CreateDirect3DDeviceManager9 來建立設備管理器。
  4. 呼叫 IDirect3DDeviceManager9::ResetDevice,在裝置管理員上設定裝置。

如果另一個管線元件需要設備管理器,它會在 EVR 上呼叫 IMFGetService::GetService,並指定服務 GUID 的MR_VIDEO_ACCELERATION_SERVICE。 EVR 會將要求傳遞給演示者。 物件取得 IDirect3DDeviceManager9 指標之後,可以呼叫 IDirect3DDeviceManager9::OpenDeviceHandle 來取得裝置的句柄。 當物件需要使用裝置時,它會將裝置句柄傳遞至 IDirect3DDeviceManager9::LockDevice 方法,此方法會傳回 IDirect3DDevice9 指標。

建立裝置之後,如果演示者終結裝置並建立新的裝置,演示者必須再次呼叫 ResetDevice ResetDevice 方法會使任何現有的裝置句柄失效,這會導致 LockDevice 傳回DXVA2_E_NEW_VIDEO_DEVICE。 此錯誤碼會使用裝置向其他對象發出訊號,指出它們應該開啟新的裝置句柄。 如需使用設備管理員的詳細資訊,請參閱 Direct3D 裝置管理員

演示者可以在視窗模式或全螢幕獨佔模式中建立裝置。 針對視窗模式,您應該為應用程式指定視訊視窗提供一種方式。 標準演示者會針對此目的實作 IMFVideoDisplayControl::SetVideoWindow 方法。 第一次建立演示者時,您必須建立裝置。 一般而言,您目前不會知道所有裝置參數,例如視窗或後台緩衝區格式。 您可以建立暫存裝置,並在稍後加以取代&#;只要記得在設備管理器上呼叫 ResetDevice 即可。

如果您建立新的裝置,或如果您在現有的裝置上呼叫 IDirect3DDevice9::Reset 或 IDirect3DDevice9Ex::ResetEx,請將EC_DISPLAY_CHANGED事件傳送至 EVR。 此事件會通知 EVR 重新談判媒體類型。 EVR 會忽略此事件的事件參數。

配置 Direct3D Surface

演示者設定媒體類型之後,即可配置 Direct3D 表面,混音器會用來寫入視訊畫面。 介面必須符合演示者的媒體類型:

  • 介面格式必須符合媒體子類型。 例如,如果子類型是 MFVideoFormat_RGB24,則介面格式必須 D3DFMT_X8R8G8B8。 如需子類型和 Direct3D 格式的詳細資訊,請參閱 影片子類型 GUID
  • 介面寬度和高度必須符合媒體類型之 MF_MT_FRAME_SIZE 屬性中指定的維度。

配置介面的建議方式取決於演示者是否執行視窗式或全螢幕。

如果 Direct3D 裝置已視窗化,您可以建立數個交換鏈結,每個交換鏈結都有單一後台緩衝區。 使用此方法,您可以獨立呈現每個介面,因為呈現一個交換鏈結不會干擾其他交換鏈結。 混音器可以將數據寫入表面,而另一個表面則排程用於呈現。

首先,決定要建立多少交換鏈結。 建議至少三個。 針對每個交換鏈結,請執行下列動作:

  1. 呼叫 IDirect3DDevice9::CreateAdditionalSwapChain 以建立交換鏈結。
  2. 呼叫 IDirect3DSwapChain9::GetBackBuffer 以取得交換鏈結後端緩衝區介面的指標。
  3. 呼叫 MFCreateVideoSampleFromSurface ,並傳入介面的指標。 此函式會傳回影片範例物件的指標。 當演示者呼叫混音器的IMFTransform::P rocessOutput方法時,影片範例物件會實作IMFSample介面,而演示者會使用此介面將表面傳遞給混音器。 如需影片範例對象的詳細資訊,請參閱 影片範例
  4. IMFSample 指標儲存在佇列中。 演示者會在處理期間從這個佇列提取範例,如處理輸出中所述
  5. 保留 IDirect3DSwapChain9 指標的參考,因此不會釋放交換鏈結。

在全螢幕獨佔模式中,裝置不能有多個交換鏈結。 當您建立全螢幕裝置時,會隱含建立此交換鏈結。 交換鏈結可以有多個後端緩衝區。 不過,不幸的是,如果您在相同的交換鏈結中寫入另一個後台緩衝區時,呈現一個後台緩衝區,則協調這兩個作業並不容易。 這是因為 Direct3D 實作表面翻轉的方式。 當您呼叫 Present 時,圖形驅動程式會更新圖形記憶體中的介面指標。 如果您在呼叫 Present 時持有任何 IDirect3DSurface9 指標,這些指標會在 Present 呼叫傳回之後指向不同的緩衝區。

最簡單的選項是建立交換鏈結的一個影片範例。 如果您選擇此選項,請遵循針對視窗模式所提供的相同步驟。 唯一的差異在於範例佇列包含單一影片範例。 另一個選項是建立螢幕外表面,然後將它們點選到後台緩衝區。 您建立的介面必須支援 IDirectXVideoProcessor::VideoProcessBlt 方法,混音器用來復合輸出畫面。

追蹤範例

當演示者第一次配置影片範例時,它會將它們放在佇列中。 每當演示者需要從混音器取得新的框架時,就會從這個佇列中繪製。 混音器輸出框架之後,演示者會將樣本移至第二個佇列。 第二個佇列是等候其排程簡報時間的範例。

為了更輕鬆地追蹤每個範例的狀態,影片範例對象會實作 IMFTrackedSample 介面。 您可以使用此介面,如下所示:

  1. 在您的 演示者中實作 IMFAsyncCallback 介面。

  2. 在排程佇列中放置範例之前,請先查詢 IMFTrackedSample 介面的影片範例物件。

  3. 使用回呼介面的指標呼叫 IMFTrackedSample::SetAllocator

  4. 當範例準備好呈現時,請從排程的佇列中移除它、呈現該範例,以及釋放範例的所有參考。

  5. 此範例會叫用回呼。 (在此案例中不會刪除範例對象,因為它會保留本身的參考計數,直到叫用回呼為止。

  6. 在回呼內,將範例傳回至可用的佇列。

演示者不需要使用 IMFTrackedSample 來追蹤樣本;您可以實作最適合您設計的任何技術。 IMFTrackedSample其中一個優點是,您可以將演示者的排程和轉譯函式移至協助程序物件,而且這些物件不需要任何特殊機制來回呼演示者,因為範例物件會提供該機制。

下列程式代碼示範如何設定回呼:

HRESULT EVRCustomPresenter::TrackSample(IMFSample *pSample)
{
    IMFTrackedSample *pTracked = NULL;

    HRESULT hr = pSample->QueryInterface(IID_PPV_ARGS(&pTracked));

    if (SUCCEEDED(hr))
    {
        hr = pTracked->SetAllocator(&m_SampleFreeCB, NULL);
    }

    SafeRelease(&pTracked);
    return hr;
}

在回呼中,在異步結果物件上呼叫 IMFAsyncResult::GetObject ,以擷取範例的指標:

HRESULT EVRCustomPresenter::OnSampleFree(IMFAsyncResult *pResult)
{
    IUnknown *pObject = NULL;
    IMFSample *pSample = NULL;
    IUnknown *pUnk = NULL;

    // Get the sample from the async result object.
    HRESULT hr = pResult->GetObject(&pObject);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pObject->QueryInterface(IID_PPV_ARGS(&pSample));
    if (FAILED(hr))
    {
        goto done;
    }

    // If this sample was submitted for a frame-step, the frame step operation
    // is complete.

    if (m_FrameStep.state == FRAMESTEP_SCHEDULED)
    {
        // Query the sample for IUnknown and compare it to our cached value.
        hr = pSample->QueryInterface(IID_PPV_ARGS(&pUnk));
        if (FAILED(hr))
        {
            goto done;
        }

        if (m_FrameStep.pSampleNoRef == (DWORD_PTR)pUnk)
        {
            // Notify the EVR.
            hr = CompleteFrameStep(pSample);
            if (FAILED(hr))
            {
                goto done;
            }
        }

        // Note: Although pObject is also an IUnknown pointer, it is not
        // guaranteed to be the exact pointer value returned through
        // QueryInterface. Therefore, the second QueryInterface call is
        // required.
    }

    /*** Begin lock ***/

    EnterCriticalSection(&m_ObjectLock);

    UINT32 token = MFGetAttributeUINT32(
        pSample, MFSamplePresenter_SampleCounter, (UINT32)-1);

    if (token == m_TokenCounter)
    {
        // Return the sample to the sample pool.
        hr = m_SamplePool.ReturnSample(pSample);
        if (SUCCEEDED(hr))
        {
            // A free sample is available. Process more data if possible.
            (void)ProcessOutputLoop();
        }
    }

    LeaveCriticalSection(&m_ObjectLock);

    /*** End lock ***/

done:
    if (FAILED(hr))
    {
        NotifyEvent(EC_ERRORABORT, hr, 0);
    }
    SafeRelease(&pObject);
    SafeRelease(&pSample);
    SafeRelease(&pUnk);
    return hr;
}

處理輸出

每當混音器收到新的輸入範例時,EVR 就會將MFVP_MESSAGE_PROCESSINPUTNOTIFY訊息傳送給演示者。 此訊息表示混音器可能有要傳遞的新視訊畫面。 作為回應,演示者在混音器上呼叫IMFTransform::P rocessOutput 如果方法成功,演示者會排程簡報的範例。

若要從混音器取得輸出,請執行下列步驟:

  1. 檢查時鐘狀態。 如果時鐘暫停,除非這是第一個視訊畫面,否則請忽略 MFVP_MESSAGE_PROCESSINPUTNOTIFY 訊息。 如果時鐘正在執行,或這是第一個視訊畫面,請繼續。

  2. 從可用範例的佇列取得範例。 如果佇列是空的,表示所有已配置的樣本目前都會排程呈現。 在此情況下,請忽略 目前MFVP_MESSAGE_PROCESSINPUTNOTIFY 訊息。 當下一個範例可供使用時,請重複此處所列的步驟。

  3. (選擇性。)如果時鐘可用,請呼叫 IMFClock::GetCorrelatedTime 來取得目前的時鐘時間 (T1)。

  4. 在混音器上呼叫IMFTransform::P rocessOutput 如果 ProcessOutput 成功,範例會包含視訊畫面。 如果方法失敗,請檢查傳回碼。 來自 ProcessOutput 的下列錯誤碼不是重大失敗:

    錯誤碼 描述
    MF_E_TRANSFORM_NEED_MORE_INPUT 混音器需要更多輸入,才能產生新的輸出框架。
    如果您收到此錯誤碼,請檢查 EVR 是否已到達數據流結尾,並據以回應,如 Stream 結尾中所述。 否則,請忽略此 MF_E_TRANSFORM_NEED_MORE_INPUT 訊息。 當混音器取得更多輸入時,EVR 會傳送另一個。
    MF_E_TRANSFORM_STREAM_CHANGE 混音器的輸出類型已變成無效,可能是因為上游格式變更。
    如果您收到此錯誤碼,請將演示者的媒體類型設定為 NULL。 EVR 會要求新的格式。
    MF_E_TRANSFORM_TYPE_NOT_SET 混音器需要新的媒體類型。
    如果您收到這個錯誤碼,請重新談判混音器的輸出類型,如交涉格式中所述

     

    如果 ProcessOutput 成功,請繼續。

  5. (選擇性。)如果時鐘可用,請取得目前的時鐘時間 (T2)。 混音器引進的延遲量為 (T2 - T1)。 將具有此值的 EC_PROCESSING_LATENCY 事件傳送至 EVR。 EVR 會使用此值進行品質控制。 如果沒有時鐘可用,就沒有理由傳送 EC_PROCESSING_LATENCY 事件。

  6. (選擇性。)查詢 IMFTrackedSample 的範例,並呼叫 IMFTrackedSample::SetAllocator,如追蹤範例中所述。

  7. 排程簡報的範例。

這個步驟序列可以在演示者取得混音器的任何輸出之前終止。 為了確保不會卸除任何要求,您應該在發生下列情況時重複相同的步驟:

  • 呼叫演示者的IMFClockStateSink::OnClockStartIMFClockStateSink::OnClockStart 方法。 這會處理混音器因為時鐘暫停而忽略輸入的情況(步驟 1)。
  • 會叫用IMFTrackedSample回呼。 這會處理混音器接收輸入,但演示者的所有影片範例都正在使用中的情況(步驟 2)。

接下來的數個程式代碼範例會更詳細地顯示這些步驟。 演示者會在取得MFVP_MESSAGE_PROCESSINPUTNOTIFY訊息時呼叫 ProcessInputNotify 方法(如下列範例所示)。

//-----------------------------------------------------------------------------
// ProcessInputNotify
//
// Attempts to get a new output sample from the mixer.
//
// This method is called when the EVR sends an MFVP_MESSAGE_PROCESSINPUTNOTIFY
// message, which indicates that the mixer has a new input sample.
//
// Note: If there are multiple input streams, the mixer might not deliver an
// output sample for every input sample.
//-----------------------------------------------------------------------------

HRESULT EVRCustomPresenter::ProcessInputNotify()
{
    HRESULT hr = S_OK;

    // Set the flag that says the mixer has a new sample.
    m_bSampleNotify = TRUE;

    if (m_pMediaType == NULL)
    {
        // We don't have a valid media type yet.
        hr = MF_E_TRANSFORM_TYPE_NOT_SET;
    }
    else
    {
        // Try to process an output sample.
        ProcessOutputLoop();
    }
    return hr;
}

這個 ProcessInputNotify 方法會設定布爾值旗標來記錄混音器有新輸入的事實。 然後它會呼叫 ProcessOutputLoop 方法,如下一個範例所示。 此方法會嘗試從混音器提取盡可能多的樣本:

void EVRCustomPresenter::ProcessOutputLoop()
{
    HRESULT hr = S_OK;

    // Process as many samples as possible.
    while (hr == S_OK)
    {
        // If the mixer doesn't have a new input sample, break from the loop.
        if (!m_bSampleNotify)
        {
            hr = MF_E_TRANSFORM_NEED_MORE_INPUT;
            break;
        }

        // Try to process a sample.
        hr = ProcessOutput();

        // NOTE: ProcessOutput can return S_FALSE to indicate it did not
        // process a sample. If so, break out of the loop.
    }

    if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT)
    {
        // The mixer has run out of input data. Check for end-of-stream.
        CheckEndOfStream();
    }
}

ProcessOutput 一個範例所示的 方法會嘗試從混音器取得單一視訊畫面。 如果沒有可用的視訊畫面, ProcessSample 則會傳回S_FALSE或錯誤碼,其中任一個會中斷 中的 ProcessOutputLoop迴圈。 大部分的工作都是在 ProcessOutput 方法內完成:

//-----------------------------------------------------------------------------
// ProcessOutput
//
// Attempts to get a new output sample from the mixer.
//
// Called in two situations:
// (1) ProcessOutputLoop, if the mixer has a new input sample.
// (2) Repainting the last frame.
//-----------------------------------------------------------------------------

HRESULT EVRCustomPresenter::ProcessOutput()
{
    assert(m_bSampleNotify || m_bRepaint);  // See note above.

    HRESULT     hr = S_OK;
    DWORD       dwStatus = 0;
    LONGLONG    mixerStartTime = 0, mixerEndTime = 0;
    MFTIME      systemTime = 0;
    BOOL        bRepaint = m_bRepaint; // Temporarily store this state flag.

    MFT_OUTPUT_DATA_BUFFER dataBuffer;
    ZeroMemory(&dataBuffer, sizeof(dataBuffer));

    IMFSample *pSample = NULL;

    // If the clock is not running, we present the first sample,
    // and then don't present any more until the clock starts.

    if ((m_RenderState != RENDER_STATE_STARTED) &&  // Not running.
         !m_bRepaint &&             // Not a repaint request.
         m_bPrerolled               // At least one sample has been presented.
         )
    {
        return S_FALSE;
    }

    // Make sure we have a pointer to the mixer.
    if (m_pMixer == NULL)
    {
        return MF_E_INVALIDREQUEST;
    }

    // Try to get a free sample from the video sample pool.
    hr = m_SamplePool.GetSample(&pSample);
    if (hr == MF_E_SAMPLEALLOCATOR_EMPTY)
    {
        // No free samples. Try again when a sample is released.
        return S_FALSE;
    }
    else if (FAILED(hr))
    {
        return hr;
    }

    // From now on, we have a valid video sample pointer, where the mixer will
    // write the video data.
    assert(pSample != NULL);

    // (If the following assertion fires, it means we are not managing the sample pool correctly.)
    assert(MFGetAttributeUINT32(pSample, MFSamplePresenter_SampleCounter, (UINT32)-1) == m_TokenCounter);

    if (m_bRepaint)
    {
        // Repaint request. Ask the mixer for the most recent sample.
        SetDesiredSampleTime(
            pSample,
            m_scheduler.LastSampleTime(),
            m_scheduler.FrameDuration()
            );

        m_bRepaint = FALSE; // OK to clear this flag now.
    }
    else
    {
        // Not a repaint request. Clear the desired sample time; the mixer will
        // give us the next frame in the stream.
        ClearDesiredSampleTime(pSample);

        if (m_pClock)
        {
            // Latency: Record the starting time for ProcessOutput.
            (void)m_pClock->GetCorrelatedTime(0, &mixerStartTime, &systemTime);
        }
    }

    // Now we are ready to get an output sample from the mixer.
    dataBuffer.dwStreamID = 0;
    dataBuffer.pSample = pSample;
    dataBuffer.dwStatus = 0;

    hr = m_pMixer->ProcessOutput(0, 1, &dataBuffer, &dwStatus);

    if (FAILED(hr))
    {
        // Return the sample to the pool.
        HRESULT hr2 = m_SamplePool.ReturnSample(pSample);
        if (FAILED(hr2))
        {
            hr = hr2;
            goto done;
        }
        // Handle some known error codes from ProcessOutput.
        if (hr == MF_E_TRANSFORM_TYPE_NOT_SET)
        {
            // The mixer's format is not set. Negotiate a new format.
            hr = RenegotiateMediaType();
        }
        else if (hr == MF_E_TRANSFORM_STREAM_CHANGE)
        {
            // There was a dynamic media type change. Clear our media type.
            SetMediaType(NULL);
        }
        else if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT)
        {
            // The mixer needs more input.
            // We have to wait for the mixer to get more input.
            m_bSampleNotify = FALSE;
        }
    }
    else
    {
        // We got an output sample from the mixer.

        if (m_pClock && !bRepaint)
        {
            // Latency: Record the ending time for the ProcessOutput operation,
            // and notify the EVR of the latency.

            (void)m_pClock->GetCorrelatedTime(0, &mixerEndTime, &systemTime);

            LONGLONG latencyTime = mixerEndTime - mixerStartTime;
            NotifyEvent(EC_PROCESSING_LATENCY, (LONG_PTR)&latencyTime, 0);
        }

        // Set up notification for when the sample is released.
        hr = TrackSample(pSample);
        if (FAILED(hr))
        {
            goto done;
        }

        // Schedule the sample.
        if ((m_FrameStep.state == FRAMESTEP_NONE) || bRepaint)
        {
            hr = DeliverSample(pSample, bRepaint);
            if (FAILED(hr))
            {
                goto done;
            }
        }
        else
        {
            // We are frame-stepping (and this is not a repaint request).
            hr = DeliverFrameStepSample(pSample);
            if (FAILED(hr))
            {
                goto done;
            }
        }

        m_bPrerolled = TRUE; // We have presented at least one sample now.
    }

done:
    SafeRelease(&pSample);

    // Important: Release any events returned from the ProcessOutput method.
    SafeRelease(&dataBuffer.pEvents);
    return hr;
}

關於此範例的一些備註:

重繪框架

有時候演示者可能需要重新畫最新的視訊畫面。 例如,標準演示者會在下列情況下重新畫出框架:

使用下列步驟來要求混音器重新建立最新的畫面:

  1. 從佇列取得影片範例。
  2. 查詢IMFDesiredSample介面的範例。
  3. 呼叫 IMFDesiredSample::SetDesiredSampleTimeAndDuration 指定最近影片畫面的時間戳。 (您必須快取此值,並針對每個畫面更新此值。
  4. 在混音器上呼叫 ProcessOutput

重繪框架時,您可以忽略簡報時鐘並立即呈現畫面。

排程範例

視訊畫面可以隨時連線到 EVR。 演示者會根據畫面的時間戳,負責在正確的時間呈現每個畫面。 當演示者從混音器取得新的樣本時,它會將樣本放在排程的佇列上。 在不同的線程上,演示者會持續從佇列前端取得第一個範例,並判斷是否:

  • 呈現範例。
  • 將範例保留在佇列上,因為它很早。
  • 捨棄範例,因為它已晚。 雖然您應該盡可能避免卸除畫面,但如果演示者持續落後,您可能需要。

若要取得視訊畫面的時間戳,請在影片範例上呼叫IMFSample::GetSampleTime 時間戳相對於EVR的簡報時鐘。 若要取得目前的時鐘時間,請呼叫 IMFClock::GetCorrelatedTime 如果 EVR 沒有簡報時鐘,或範例沒有時間戳,您可以在取得範例之後立即呈現範例。

若要取得每個範例的持續時間,請呼叫IMFSample::GetSampleDuration 如果範例沒有持續時間,您可以使用 MFFrameRateToAverageTimePerFrame 函式來計算幀速率的持續時間。

當您排程範例時,請記住下列事項:

  • 如果播放速率比正常速度快或慢,則時鐘會以更快的速度或較慢的速度執行。 這表示範例上的時間戳一律會提供相對於簡報時鐘的正確目標時間。 不過,如果您將簡報時間轉譯為其他時鐘時間(例如高解析度性能計數器),則必須根據時鐘速度來調整時間。 如果時鐘速度變更,EVR 會呼叫演示者的 IMFClockStateSink::OnClockSetRate 方法。
  • 反向播放的播放速率可以是負數。 當播放速率為負數時,簡報時鐘會向後執行。 換句話說,N + 1 發生在 N 時間之前。

下列範例會計算範例相對於簡報時鐘的早期或遲到程度:

    LONGLONG hnsPresentationTime = 0;
    LONGLONG hnsTimeNow = 0;
    MFTIME   hnsSystemTime = 0;

    BOOL bPresentNow = TRUE;
    LONG lNextSleep = 0;

    if (m_pClock)
    {
        // Get the sample's time stamp. It is valid for a sample to
        // have no time stamp.
        hr = pSample->GetSampleTime(&hnsPresentationTime);

        // Get the clock time. (But if the sample does not have a time stamp,
        // we don't need the clock time.)
        if (SUCCEEDED(hr))
        {
            hr = m_pClock->GetCorrelatedTime(0, &hnsTimeNow, &hnsSystemTime);
        }

        // Calculate the time until the sample's presentation time.
        // A negative value means the sample is late.
        LONGLONG hnsDelta = hnsPresentationTime - hnsTimeNow;
        if (m_fRate < 0)
        {
            // For reverse playback, the clock runs backward. Therefore, the
            // delta is reversed.
            hnsDelta = - hnsDelta;
        }

簡報時鐘通常是由系統時鐘或音訊轉譯器驅動。 (音訊轉譯器會從聲卡取用音訊的速率衍生時間。一般而言,簡報時鐘不會與監視器的重新整理速率同步。

如果您的 Direct3D 簡報參數指定 簡報間隔的D3DPRESENT_INTERVAL_DEFAULTD3DPRESENT_INTERVAL_ONE則 Present 作業會等候監視器的垂直回溯。 這是防止撕裂的簡單方法,但可減少排程演算法的精確度。 相反地,如果簡報間隔是 D3DPRESENT_INTERVAL_IMMEDIATE則 Present 方法會立即執行,這會導致撕裂,除非您的排程演算法精確度足以讓您只在垂直回溯期間呼叫 Present

下列函式可協助您取得精確的計時資訊:

  • IDirect3DDevice9::GetRasterStatus 會傳回點陣的相關信息,包括目前的掃描線,以及點陣是否處於垂直空白期間。
  • DwmGetCompositionTimingInfo 會傳回桌面視窗管理員的計時資訊。 如果已啟用桌面組合,這項資訊就很有用。

呈現範例

本節假設您已為每個介面建立個別的交換鏈結,如配置 Direct3D Surface 中所述。 若要呈現範例,請從影片範例取得交換鏈結,如下所示:

  1. 在影片範例上呼叫 IMFSample::GetBufferByIndex 以取得緩衝區。
  2. 查詢IMFGetService介面的緩衝區。
  3. 呼叫 IMFGetService::GetService 以取得 Direct3D 介面的 IDirect3DSurface9 介面。 (您可以藉由呼叫 ,將此步驟和上一個步驟合併成一個步驟MFGetService.)
  4. 在介面上呼叫 IDirect3DSurface9::GetContainer 以取得交換鏈結的指標。
  5. 在交換鏈結上呼叫 IDirect3DSwapChain9::P resent

下列程式代碼顯示下列步驟:

HRESULT D3DPresentEngine::PresentSample(IMFSample* pSample, LONGLONG llTarget)
{
    HRESULT hr = S_OK;

    IMFMediaBuffer* pBuffer = NULL;
    IDirect3DSurface9* pSurface = NULL;
    IDirect3DSwapChain9* pSwapChain = NULL;

    if (pSample)
    {
        // Get the buffer from the sample.
        hr = pSample->GetBufferByIndex(0, &pBuffer);
        if (FAILED(hr))
        {
            goto done;
        }

        // Get the surface from the buffer.
        hr = MFGetService(pBuffer, MR_BUFFER_SERVICE, IID_PPV_ARGS(&pSurface));
        if (FAILED(hr))
        {
            goto done;
        }
    }
    else if (m_pSurfaceRepaint)
    {
        // Redraw from the last surface.
        pSurface = m_pSurfaceRepaint;
        pSurface->AddRef();
    }

    if (pSurface)
    {
        // Get the swap chain from the surface.
        hr = pSurface->GetContainer(IID_PPV_ARGS(&pSwapChain));
        if (FAILED(hr))
        {
            goto done;
        }

        // Present the swap chain.
        hr = PresentSwapChain(pSwapChain, pSurface);
        if (FAILED(hr))
        {
            goto done;
        }

        // Store this pointer in case we need to repaint the surface.
        CopyComPointer(m_pSurfaceRepaint, pSurface);
    }
    else
    {
        // No surface. All we can do is paint a black rectangle.
        PaintFrameWithGDI();
    }

done:
    SafeRelease(&pSwapChain);
    SafeRelease(&pSurface);
    SafeRelease(&pBuffer);

    if (FAILED(hr))
    {
        if (hr == D3DERR_DEVICELOST || hr == D3DERR_DEVICENOTRESET || hr == D3DERR_DEVICEHUNG)
        {
            // We failed because the device was lost. Fill the destination rectangle.
            PaintFrameWithGDI();

            // Ignore. We need to reset or re-create the device, but this method
            // is probably being called from the scheduler thread, which is not the
            // same thread that created the device. The Reset(Ex) method must be
            // called from the thread that created the device.

            // The presenter will detect the state when it calls CheckDeviceState()
            // on the next sample.
            hr = S_OK;
        }
    }
    return hr;
}

來源和目的地矩形

來源 矩形 是要顯示的視訊畫面部分。 其定義相對於標準化座標系統,其中整個視訊畫面佔用具有座標 {0, 0, 1, 1} 的矩形。 目的 矩形 是繪製視訊框架之目的介面內的區域。 標準演示者可讓應用程式呼叫 IMFVideoDisplayControl::SetVideoPosition 來設定這些矩形。

有數個選項可用來套用來源和目的地矩形。 第一個選項是讓混音器套用它們:

  • 使用 VIDEO_ZOOM_RECT 屬性設定來源矩形。 混音器會在將視訊點入目的地表面時,套用來源矩形。 混音器的預設來源矩形是整個框架。
  • 將目的地矩形設定為混音器輸出類型中的幾何孔徑。 如需詳細資訊,請參閱 交涉格式

第二個選項是藉由在 Present 方法中指定 pSourceRect 和 pDestRect 參數,在 IDirect3DSwapChain9::P resent套用矩形。 您可以結合這些選項。 例如,您可以在混音器上設定來源矩形,但在 Present 方法中套用目的地矩形。

如果應用程式變更目的地矩形或調整視窗大小,您可能需要配置新的介面。 若是如此,您必須小心將此作業與您的排程線程同步處理。 排清排程佇列,並在配置新介面之前捨棄舊的範例。

數據流結尾

當EVR上的每個輸入數據流都結束時,EVR 會將MFVP_MESSAGE_ENDOFSTREAM訊息傳送給演示者。 不過,當您收到訊息時,可能還有一些待處理的視訊畫面。 在響應資料流結尾訊息之前,您必須清空混音器的所有輸出,並呈現其餘所有畫面格。 顯示最後一個 畫面之後,將EC_COMPLETE 事件傳送至 EVR。

下一個範例顯示如果符合各種條件, 則會傳送EC_COMPLETE 事件的方法。 否則,它會傳回S_OK而不傳送事件:

HRESULT EVRCustomPresenter::CheckEndOfStream()
{
    if (!m_bEndStreaming)
    {
        // The EVR did not send the MFVP_MESSAGE_ENDOFSTREAM message.
        return S_OK;
    }

    if (m_bSampleNotify)
    {
        // The mixer still has input.
        return S_OK;
    }

    if (m_SamplePool.AreSamplesPending())
    {
        // Samples are still scheduled for rendering.
        return S_OK;
    }

    // Everything is complete. Now we can tell the EVR that we are done.
    NotifyEvent(EC_COMPLETE, (LONG_PTR)S_OK, 0);
    m_bEndStreaming = FALSE;
    return S_OK;
}

此方法會檢查下列狀態:

  • 如果m_fSampleNotify變數為 TRUE,表示混音器有一或多個尚未處理的畫面。 (如需詳細資訊,請參閱 處理輸出。)
  • m_fEndStreaming變數是布爾值旗標,其初始值 FALSE。 當EVR傳送MFVP_MESSAGE_ENDOFSTREAM訊息時,演示者會將旗標設定為TRUE
  • 只要一或多個框架在排程佇列中等候,方法 AreSamplesPending 就會假設傳回 TRUE

IMFVideoPresenter::P rocessMessage方法中,將m_fEndStreaming設定TRUE,並在EE,並在EVR傳送MFVP_MESSAGE_ENDOFSTREAM訊息時呼叫CheckEndOfStream

HRESULT EVRCustomPresenter::ProcessMessage(
    MFVP_MESSAGE_TYPE eMessage,
    ULONG_PTR ulParam
    )
{
    HRESULT hr = S_OK;

    EnterCriticalSection(&m_ObjectLock);

    hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    switch (eMessage)
    {
    // Flush all pending samples.
    case MFVP_MESSAGE_FLUSH:
        hr = Flush();
        break;

    // Renegotiate the media type with the mixer.
    case MFVP_MESSAGE_INVALIDATEMEDIATYPE:
        hr = RenegotiateMediaType();
        break;

    // The mixer received a new input sample.
    case MFVP_MESSAGE_PROCESSINPUTNOTIFY:
        hr = ProcessInputNotify();
        break;

    // Streaming is about to start.
    case MFVP_MESSAGE_BEGINSTREAMING:
        hr = BeginStreaming();
        break;

    // Streaming has ended. (The EVR has stopped.)
    case MFVP_MESSAGE_ENDSTREAMING:
        hr = EndStreaming();
        break;

    // All input streams have ended.
    case MFVP_MESSAGE_ENDOFSTREAM:
        // Set the EOS flag.
        m_bEndStreaming = TRUE;
        // Check if it's time to send the EC_COMPLETE event to the EVR.
        hr = CheckEndOfStream();
        break;

    // Frame-stepping is starting.
    case MFVP_MESSAGE_STEP:
        hr = PrepareFrameStep(LODWORD(ulParam));
        break;

    // Cancels frame-stepping.
    case MFVP_MESSAGE_CANCELSTEP:
        hr = CancelFrameStep();
        break;

    default:
        hr = E_INVALIDARG; // Unknown message. This case should never occur.
        break;
    }

done:
    LeaveCriticalSection(&m_ObjectLock);
    return hr;
}

此外,如果混音器的IMFTransform::P rocessOutput方法傳回MF_E_TRANSFORM_NEED_MORE_INPUT,則呼叫 CheckEndOfStream 這個錯誤碼表示混音器沒有更多的輸入樣本(請參閱 處理輸出)。

框架逐步執行

EVR 的設計目的是支援 DirectShow 中的框架逐步執行,並在媒體基礎中清除。 框架逐步執行和清除在概念上很類似。 在這兩種情況下,應用程式一次要求一個視訊畫面。 在內部,演示者會使用相同的機制來實作這兩個功能。

DirectShow 中的框架逐步執行的運作方式如下:

  • 應用程式會呼叫 IVideoFrameStep::Step。 dwSteps 參數中會提供步驟數目。 EVR 會將MFVP_MESSAGE_STEP訊息傳送至演示者,其中 message 參數 (ulParam) 是步驟數目。
  • 如果應用程式呼叫 IVideoFrameStep::CancelStep 或變更圖形狀態(執行中、暫停或已停止),EVR 會傳送MFVP_MESSAGE_CANCELSTEP訊息。

媒體基礎中的清除運作方式如下:

  • 應用程式會呼叫 IMFRateControl::SetRate,將播放速率設定為零。
  • 若要轉譯新的框架,應用程式會呼叫 IMFMediaSession::Start 與所需的位置。 EVR 會傳送 MFVP_MESSAGE_STEP 訊息, ulParam 等於 1。
  • 若要停止清除,應用程式會將播放速率設定為非零值。 EVR 會傳送 MFVP_MESSAGE_CANCELSTEP 訊息。

收到 MFVP_MESSAGE_STEP 訊息之後,演示者會等候目標框架到達。 如果步驟數目為 N,演示者會捨棄下一個 (N - 1) 樣本,並呈現第 N樣本。 當演示者完成框架步驟時,它會將EC_STEP_COMPLETE事件傳送至 EVR,並將 lParam1 設定為 FALSE。 此外,如果播放速率為零,演示者會傳送 EC_SCRUB_TIME 事件。 如果EVR在框架步驟作業仍在擱置時取消框架逐步執行,則演示者會將 lParam1 設定為 TRUE 的EC_STEP_COMPLETE事件傳送

應用程式可以框架步驟或清除多次,因此演示者可能會在取得MFVP_MESSAGE_CANCELSTEP訊息之前收到多個MFVP_MESSAGE_STEP訊息。 此外,演示者可以在時鐘開始或時鐘執行之前接收 MFVP_MESSAGE_STEP 訊息。

實作框架逐步執行

本節說明實作框架逐步執行的演算法。 框架逐步執行演算法會使用下列變數:

  • step_count。 不帶正負號的整數,指定目前框架逐步執行作業中的步驟數目。

  • step_queue。 IMFSample 指標的佇列。

  • step_state。 每當演示者在框架逐步執行時,都可以處於下列其中一種狀態:

    State 描述
    NOT_STEPPING 不是框架逐步執行。
    演示者已 收到MFVP_MESSAGE_STEP 訊息,但時鐘尚未啟動。
    PENDING 演示者已 收到MFVP_MESSAGE_STEP 訊息,而且時鐘已啟動,但演示者正在等候接收目標畫面。
    計劃 演示者已收到目標框架,並已排程簡報,但尚未呈現框架。
    完成 演示者已呈現目標框架並傳送 EC_STEP_COMPLETE 事件,並正在等候下一個 MFVP_MESSAGE_STEPMFVP_MESSAGE_CANCELSTEP 訊息。

     

    這些狀態與演示者狀態一節 中列出的演示者狀態無關。

下列程式是針對框架逐步執行演算法所定義:

PrepareFrameStep 程式

  1. 遞增 step_count
  2. 將 [step_state] 設定為 [等候]。
  3. 如果時鐘正在執行,請呼叫 StartFrameStep。

StartFrameStep 程式

  1. 如果step_state等於 WAITING,請將step_state設定為 PENDING。 針對 step_queue 中的每個範例,呼叫 DeliverFrameStepSample。
  2. 如果 step_state 等於NOT_STEPPING,請從 step_queue 移除任何範例,並排程它們進行簡報。

CompleteFrameStep 程式

  1. 將step_state設定為 COMPLETE。
  2. 使用 lParam1 = FALSE 傳送EC_STEP_COMPLETE事件。
  3. 如果時鐘速率為零,請使用取樣時間傳送 EC_SCRUB_TIME 事件。

DeliverFrameStepSample 程式

  1. 如果時鐘速率為零,且取樣時間 + 樣本持續時間<的時鐘時間,請捨棄範例。 結束。
  2. 如果 step_state 等於 SCHEDULED 或 COMPLETE,請將範例新增至 step_queue。 結束。
  3. 遞減 step_count
  4. 如果 step_count> 0,請捨棄範例。 結束。
  5. 如果 step_state 等於 WAITING,請將範例新增至 step_queue。 結束。
  6. 排程簡報的範例。
  7. 將step_state設定為 SCHEDULED。

CancelFrameStep 程式

  1. 將 [step_state] 設定為 [NOT_STEPPING
  2. 將step_count重設為零。
  3. 如果先前的 step_state 值為 WAITING、PENDING 或 SCHEDULED,請使用 lParam1 = TRUE 傳送EC_STEP_COMPLETE。

呼叫下列程式,如下所示:

演示者訊息或方法 程序
MFVP_MESSAGE_STEP訊息 PrepareFrameStep
MFVP_MESSAGE_STEP訊息 CancelStep
IMFClockStateSink::OnClockStart StartFrameStep
IMFClockStateSink::OnClockRestart StartFrameStep
IMFTrackedSample 回呼 CompleteFrameStep
IMFClockStateSink::OnClockStop CancelFrameStep
IMFClockStateSink::OnClockSetRate CancelFrameStep

 

下列流程圖顯示框架逐步執行程式。

flow chart showing paths that start with mfvp-message-step and mfvp-message-processinputnotify and end at

在 EVR 上設定演示者

實作演示者之後,下一個步驟是將 EVR 設定為使用它。

在 DirectShow 中設定演示者

在 DirectShow 應用程式中,在 EVR 上設定演示者,如下所示:

  1. 呼叫 CoCreateInstance 以建立 EVR 篩選條件。 CLSID CLSID_EnhancedVideoRenderer
  2. 將 EVR 新增至篩選圖表。
  3. 建立演示者的實例。 演示者可以透過 IClassFactory 支援標準 COM 物件建立,但這並非必要專案。
  4. 查詢IMFVideoRenderer介面的EVR篩選條件。
  5. 呼叫 IMFVideoRenderer::InitializeRenderer

在媒體基礎中設定演示者

在媒體基礎中,您會有數個選項,視您建立 EVR 媒體接收或 EVR 啟用物件而定。 如需啟用對象的詳細資訊,請參閱 Activation Objects

針對EVR媒體接收,請執行下列動作:

  1. 呼叫 MFCreateVideoRenderer 以建立媒體接收。
  2. 建立演示者的實例。
  3. 查詢IMFVideoRenderer介面的EVR媒體接收。
  4. 呼叫 IMFVideoRenderer::InitializeRenderer

針對 EVR 啟用物件,請執行下列動作:

  1. 呼叫 MFCreateVideoRendererActivate 以建立啟用物件。

  2. 在啟用物件上設定下列其中一個屬性:

    屬性 描述
    MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_ACTIVATE 演示者的啟用物件的指標。
    使用此旗標,您必須為演示者提供啟用物件。 啟用對象必須實 作IMFActivate 介面。
    MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_CLSID 演示者的 CLSID。
    使用此旗標,演示者必須支援透過 IClassFactory 建立標準 COM 物件。

     

  3. 或者,在啟用對象上設定 MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_FLAGS 屬性。

增強式視訊轉譯器

EVRPresenter 範例