共用方式為


案例研究:MPEG-1 媒體來源

在 Microsoft Media Foundation 中,將媒體資料導入資料管線的物件稱為 媒體來源。 本主題會深入探討 MPEG-1 媒體來源 SDK 範例。

必要條件

閱讀本主題之前,您應該先瞭解下列媒體基礎概念:

您也應該對媒體基礎架構有基本的瞭解,特別是管線中媒體來源的角色。 (如需詳細資訊,請參閱 媒體來源.)

此外,您可能想要閱讀 撰寫自訂媒體來源主題,其中提供這裡所述的步驟更一般概觀。

本主題不會從 SDK 範例重現所有程式碼,因為範例相當大。

MPEG-1 來源中使用的 C++ 類別

範例 MPEG-1 來源是使用下列 C++ 類別來實作:

  • MPEG1ByteStreamHandler. 實作媒體來源的位元組資料流程處理常式。 指定位元組資料流程時,位元組資料流程處理常式會建立來源的實例。
  • MPEG1Source. 實作媒體來源。
  • MPEG1Stream. 實作媒體資料流程物件。 媒體來源會針對 MPEG-1 位資料流程中的每個音訊或視訊資料流程建立一個 MPEG1Stream 物件。
  • Parser. 剖析 MPEG-1 位資料流程。 在大部分情況下,此類別的詳細資料與媒體基礎 API 無關。
  • SourceOp、OpQueue:這兩個類別會管理媒體來源中的非同步作業。 (請參閱 非同步作業) 。

本主題稍後會說明其他協助程式類別。

Byte-Stream處理常式

位元組資料流程處理常式是建立媒體來源的物件。 位元組資料流程處理常式是由來源解析程式所建立;應用程式不會直接與位元組資料流程處理常式互動。 來源解析程式會藉由查看登錄來探索位元組資料流程處理常式。 處理常式會依副檔名或 MIME 類型註冊。 針對 MPEG-1 來源,位元組資料流程處理常式會註冊 「.mpg」 副檔名。

注意

如果您想要支援自訂 URL 配置,您也可以撰寫 配置處理常式。 MPEG-1 來源是針對本機檔案所設計,而 Media Foundation 已提供 「file://」 URL 的配置處理常式。

 

位元組資料流程處理常式會實作 IMFByteStreamHandler 介面。 此介面有兩個必須實作的最重要方法:

其他兩種方法是選擇性方法,但未在 SDK 範例中實作:

  • CancelObjectCreation。 取消 BeginCreateObject 方法。 這個方法適用于在啟動時可能會有高延遲的網路來源。
  • GetMaxNumberOfBytesRequiredForResolution。 取得處理常式將從來來源資料流讀取的最大位元組數目。 如果您知道位元組資料流程處理常式在可以建立媒體來源之前有多少資料,請實作這個方法。 否則,只要傳回 E_NOTIMPL

以下是 BeginCreateObject 方法的實作:

HRESULT MPEG1ByteStreamHandler::BeginCreateObject(
        /* [in] */ IMFByteStream *pByteStream,
        /* [in] */ LPCWSTR pwszURL,
        /* [in] */ DWORD dwFlags,
        /* [in] */ IPropertyStore *pProps,
        /* [out] */ IUnknown **ppIUnknownCancelCookie,  // Can be NULL
        /* [in] */ IMFAsyncCallback *pCallback,
        /* [in] */ IUnknown *punkState                  // Can be NULL
        )
{
    if (pByteStream == NULL)
    {
        return E_POINTER;
    }

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

    if ((dwFlags & MF_RESOLUTION_MEDIASOURCE) == 0)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = S_OK;

    IMFAsyncResult *pResult = NULL;
    MPEG1Source    *pSource = NULL;

    // Create an instance of the media source.
    hr = MPEG1Source::CreateInstance(&pSource);

    // Create a result object for the caller's async callback.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateAsyncResult(NULL, pCallback, punkState, &pResult);
    }

    // Start opening the source. This is an async operation.
    // When it completes, the source will invoke our callback
    // and then we will invoke the caller's callback.
    if (SUCCEEDED(hr))
    {
        hr = pSource->BeginOpen(pByteStream, this, NULL);
    }

    if (SUCCEEDED(hr))
    {
        if (ppIUnknownCancelCookie)
        {
            *ppIUnknownCancelCookie = NULL;
        }

        m_pSource = pSource;
        m_pSource->AddRef();

        m_pResult = pResult;
        m_pResult->AddRef();
    }

// cleanup
    SafeRelease(&pSource);
    SafeRelease(&pResult);
    return hr;
}

方法會執行下列步驟:

  1. 建立 MPEG1Source 物件的新執行個體。
  2. 建立異步結果物件。 稍後會使用此物件來叫用來源解析程式的回呼方法。
  3. 呼叫 MPEG1Source::BeginOpen ,這個非同步方法定義于 類別中 MPEG1Source
  4. ppIUnknownCancelCookie 設定為 Null,通知呼叫端不支援 CancelObjectCreation

方法 MPEG1Source::BeginOpen 會執行讀取位元組資料流程並初始化 MPEG1Source 物件的實際工作。 這個方法不是公用 API 的一部分。 您可以在處理常式與符合您需求的媒體來源之間定義任何機制。 將大部分的邏輯放入媒體來源會讓位元組資料流程處理常式相對簡單。

簡短來說, BeginOpen 請執行下列動作:

  1. 呼叫 IMFByteStream::GetCapabilities ,以確認來源位元組資料流程是可讀取且可搜尋的。
  2. 呼叫 IMFByteStream::BeginRead 以啟動非同步 I/O 要求。

初始化的其餘部分會以非同步方式發生。 媒體來源會從資料流程讀取足夠的資料,以剖析 MPEG-1 序列標頭。 然後它會建立 簡報描述元,這是用來描述檔案中音訊和視訊資料流程的物件。 (如需詳細資訊,請參閱 Presentation Descriptor.) 當 BeginOpen 作業完成時,位元組資料流程處理常式會叫用來源解析程式的回呼方法。 此時,來源解析程式會呼叫 IMFByteStreamHandler::EndCreateObjectEndCreateObject方法會傳回作業的狀態。

HRESULT MPEG1ByteStreamHandler::EndCreateObject(
        /* [in] */ IMFAsyncResult *pResult,
        /* [out] */ MF_OBJECT_TYPE *pObjectType,
        /* [out] */ IUnknown **ppObject)
{
    if (pResult == NULL || pObjectType == NULL || ppObject == NULL)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;

    *pObjectType = MF_OBJECT_INVALID;
    *ppObject = NULL;

    hr = pResult->GetStatus();

    if (SUCCEEDED(hr))
    {
        *pObjectType = MF_OBJECT_MEDIASOURCE;

        assert(m_pSource != NULL);

        hr = m_pSource->QueryInterface(IID_PPV_ARGS(ppObject));
    }

    SafeRelease(&m_pSource);
    SafeRelease(&m_pResult);
    return hr;
}

如果此程式期間隨時發生錯誤,則會使用錯誤狀態碼叫用回呼。

簡報描述元

簡報描述項描述 MPEG-1 檔案的內容,包括下列資訊:

  • 資料流程的數目。
  • 每個資料流程的格式。
  • 資料流程識別碼。
  • 每個資料流程的選取狀態 (選取或取消選取) 。

就媒體基礎架構而言,簡報描述元包含一或多個 資料流程描述元。 每個資料流程描述項都包含 媒體類型處理常式,可用來取得或設定資料流程上的媒體類型。 媒體基礎提供簡報描述元和串流描述元的庫存實作;這些適用于大部分媒體來源。

若要建立簡報描述元,請執行下列步驟:

  1. 針對每個資料流程:
    1. 提供資料流程識別碼和可能媒體類型的陣列。 如果資料流程支援多個媒體類型,請依喜好設定排序媒體類型清單,如果有的話。 (先放置最佳類型,最後一個最理想的類型為 last.)
    2. 呼叫 MFCreateStreamDescriptor 以建立資料流程描述元。
    3. 在新建立的資料流程描述元上呼叫 IMFStreamDescriptor::GetMediaTypeHandler
    4. 呼叫 IMFMediaTypeHandler::SetCurrentMediaType 來設定資料流程的預設格式。 如果有一個以上的媒體類型,您通常會在清單中設定第一個類型。
  2. 呼叫 MFCreatePresentationDescriptor 並傳入資料流程描述元指標的陣列。
  3. 針對每個資料流程,呼叫 IMFPresentationDescriptor::SelectStreamDeselectStream 來設定預設選取狀態。 如果有多個相同類型的資料流程 (音訊或視訊) ,則預設只能選取一個資料流程。

物件 MPEG1Source 會在其 InitPresentationDescriptor 方法中建立簡報描述元:

HRESULT MPEG1Source::InitPresentationDescriptor()
{
    HRESULT hr = S_OK;
    DWORD cStreams = 0;

    assert(m_pPresentationDescriptor == NULL);
    assert(m_state == STATE_OPENING);

    if (m_pHeader == NULL)
    {
        return E_FAIL;
    }

    // Get the number of streams, as declared in the MPEG-1 header, skipping
    // any streams with an unsupported format.
    for (DWORD i = 0; i < m_pHeader->cStreams; i++)
    {
        if (IsStreamTypeSupported(m_pHeader->streams[i].type))
        {
            cStreams++;
        }
    }

    // How many streams do we actually have?
    if (cStreams > m_streams.GetCount())
    {
        // Keep reading data until we have seen a packet for each stream.
        return S_OK;
    }

    // We should never create a stream we don't support.
    assert(cStreams == m_streams.GetCount());

    // Ready to create the presentation descriptor.

    // Create an array of IMFStreamDescriptor pointers.
    IMFStreamDescriptor **ppSD =
            new (std::nothrow) IMFStreamDescriptor*[cStreams];

    if (ppSD == NULL)
    {
        hr = E_OUTOFMEMORY;
        goto done;
    }

    ZeroMemory(ppSD, cStreams * sizeof(IMFStreamDescriptor*));

    // Fill the array by getting the stream descriptors from the streams.
    for (DWORD i = 0; i < cStreams; i++)
    {
        hr = m_streams[i]->GetStreamDescriptor(&ppSD[i]);
        if (FAILED(hr))
        {
            goto done;
        }
    }

    // Create the presentation descriptor.
    hr = MFCreatePresentationDescriptor(cStreams, ppSD,
        &m_pPresentationDescriptor);

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

    // Select the first video stream (if any).
    for (DWORD i = 0; i < cStreams; i++)
    {
        GUID majorType = GUID_NULL;

        hr = GetStreamMajorType(ppSD[i], &majorType);
        if (FAILED(hr))
        {
            goto done;
        }

        if (majorType == MFMediaType_Video)
        {
            hr = m_pPresentationDescriptor->SelectStream(i);
            if (FAILED(hr))
            {
                goto done;
            }
            break;
        }
    }

    // Switch state from "opening" to stopped.
    m_state = STATE_STOPPED;

    // Invoke the async callback to complete the BeginOpen operation.
    hr = CompleteOpen(S_OK);

done:
    // clean up:
    if (ppSD)
    {
        for (DWORD i = 0; i < cStreams; i++)
        {
            SafeRelease(&ppSD[i]);
        }
        delete [] ppSD;
    }
    return hr;
}

應用程式會呼叫 IMFMediaSource::CreatePresentationDescriptor來取得簡報描述元。 此方法會呼叫 IMFPresentationDescriptor::Clone,以建立簡報描述項的淺層複本。 (複本包含原始資料流程描述項的指標。) 應用程式可以使用簡報描述元來設定媒體類型、選取資料流程或取消選取資料流程。

或者,簡報描述項和資料流程描述元可以包含屬性,以提供來源的其他資訊。 如需這類屬性的清單,請參閱下列主題:

一個屬性值得特別提及: MF_PD_DURATION 屬性包含來源的總持續時間。 如果您知道前置持續時間,請設定此屬性;例如,視檔案格式而定,可能會在檔頭中指定持續時間。 應用程式可能會顯示此值,或使用它來設定進度列或搜尋列。

串流狀態

媒體來源會定義下列狀態:

State 描述
已開始 來源會接受和處理範例要求。
已暫停 來源接受範例要求,但不會處理它們。 要求會排入佇列,直到來源啟動為止。
已停止。 來源會拒絕範例要求。

 

開始

IMFMediaSource::Start方法會啟動媒體來源。 它需要以下參數:

  • 簡報描述項。
  • 時間格式 GUID。
  • 開始位置。

應用程式必須在來源上呼叫 CreatePresentationDescriptor ,以取得簡報描述元。 沒有用來驗證簡報描述元的定義機制。 如果應用程式指定錯誤的簡報描述元,則結果未定義。

時間格式 GUID 會指定如何解譯起始位置。 標準格式為 100 奈秒 (ns) 單位,以 GUID_Null 表示。 每個媒體來源都必須支援 100-ns 個單位。 或者,來源可以支援其他時間單位,例如框架編號或時間代碼。 不過,沒有標準方式可以查詢媒體來源,以取得其支援的時間格式清單。

起始位置會指定為 PROPVARIANT,根據時間格式允許不同的資料類型。 針對 100-ns, PROPVARIANT 類型為 VT_I8VT_EMPTY。 如果 VT_I8PROPVARIANT 會以 100-ns 單位包含開始位置。 值VT_EMPTY具有「從目前位置開始」的特殊意義。

實作 Start 方法,如下所示:

  1. 驗證參數和狀態:
    • 檢查 Null 參數。
    • 檢查時間格式 GUID。 如果值無效,則傳回 MF_E_UNSUPPORTED_TIME_FORMAT
    • 檢查保存開始位置之 PROPVARIANT 的資料類型。
    • 驗證開始位置。 如果無效,則傳回 MF_E_INVALIDREQUEST
    • 如果來源已關閉,請 傳回MF_E_SHUTDOWN
  2. 如果步驟 1 中沒有發生錯誤,請將非同步作業排入佇列。 此步驟之後的所有專案都會在工作佇列執行緒上發生。
  3. 針對每個資料流程:
    1. 檢查資料流程是否已在先前的 [開始 ] 要求中。

    2. 呼叫 IMFPresentationDescriptor::GetStreamDescriptorByIndex 來檢查應用程式是否已選取或取消選取資料流程。

    3. 如果先前選取的資料流程現在已取消選取,請清除該資料流程的任何未傳遞樣本。

    4. 如果資料流程作用中,媒體來源 (不是資料流程) 傳送下列其中一個事件:

      針對這兩個事件,事件資料是資料流程的 IMFMediaStream 指標。

    5. 如果來源從暫停狀態重新開機,可能會有擱置的範例要求。 如果是,請立即提供這些專案。

    6. 如果來源正在搜尋新的位置,則每個資料流程物件都會傳送 MEStreamSeeked 事件。 否則,每個資料流程都會傳送 MEStreamStarted 事件。

  4. 如果來源正在搜尋新的位置,媒體來源會傳送 MESourceSeeked 事件。 否則,它會傳送 MESourceStarted 事件。

如果在步驟 2 之後的任何時間發生錯誤,來源就會傳送具有錯誤碼的 MESourceStarted 事件。 這會向應用程式警示 Start 方法非同步失敗。

下列程式碼顯示步驟 1-2:

HRESULT MPEG1Source::Start(
        IMFPresentationDescriptor* pPresentationDescriptor,
        const GUID* pguidTimeFormat,
        const PROPVARIANT* pvarStartPos
    )
{

    HRESULT hr = S_OK;
    SourceOp *pAsyncOp = NULL;

    // Check parameters.

    // Start position and presentation descriptor cannot be NULL.
    if (pvarStartPos == NULL || pPresentationDescriptor == NULL)
    {
        return E_INVALIDARG;
    }

    // Check the time format.
    if ((pguidTimeFormat != NULL) && (*pguidTimeFormat != GUID_NULL))
    {
        // Unrecognized time format GUID.
        return MF_E_UNSUPPORTED_TIME_FORMAT;
    }

    // Check the data type of the start position.
    if ((pvarStartPos->vt != VT_I8) && (pvarStartPos->vt != VT_EMPTY))
    {
        return MF_E_UNSUPPORTED_TIME_FORMAT;
    }

    EnterCriticalSection(&m_critSec);

    // Check if this is a seek request. This sample does not support seeking.

    if (pvarStartPos->vt == VT_I8)
    {
        // If the current state is STOPPED, then position 0 is valid.
        // Otherwise, the start position must be VT_EMPTY (current position).

        if ((m_state != STATE_STOPPED) || (pvarStartPos->hVal.QuadPart != 0))
        {
            hr = MF_E_INVALIDREQUEST;
            goto done;
        }
    }

    // Fail if the source is shut down.
    hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    // Fail if the source was not initialized yet.
    hr = IsInitialized();
    if (FAILED(hr))
    {
        goto done;
    }

    // Perform a basic check on the caller's presentation descriptor.
    hr = ValidatePresentationDescriptor(pPresentationDescriptor);
    if (FAILED(hr))
    {
        goto done;
    }

    // The operation looks OK. Complete the operation asynchronously.
    hr = SourceOp::CreateStartOp(pPresentationDescriptor, &pAsyncOp);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pAsyncOp->SetData(*pvarStartPos);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = QueueOperation(pAsyncOp);

done:
    SafeRelease(&pAsyncOp);
    LeaveCriticalSection(&m_critSec);
    return hr;
}

下一個範例會顯示其餘步驟:

HRESULT MPEG1Source::DoStart(StartOp *pOp)
{
    assert(pOp->Op() == SourceOp::OP_START);

    IMFPresentationDescriptor *pPD = NULL;
    IMFMediaEvent  *pEvent = NULL;

    HRESULT     hr = S_OK;
    LONGLONG    llStartOffset = 0;
    BOOL        bRestartFromCurrentPosition = FALSE;
    BOOL        bSentEvents = FALSE;

    hr = BeginAsyncOp(pOp);

    // Get the presentation descriptor from the SourceOp object.
    // This is the PD that the caller passed into the Start() method.
    // The PD has already been validated.
    if (SUCCEEDED(hr))
    {
        hr = pOp->GetPresentationDescriptor(&pPD);
    }

    // Because this sample does not support seeking, the start
    // position must be 0 (from stopped) or "current position."

    // If the sample supported seeking, we would need to get the
    // start position from the PROPVARIANT data contained in pOp.

    if (SUCCEEDED(hr))
    {
        // Select/deselect streams, based on what the caller set in the PD.
        // This method also sends the MENewStream/MEUpdatedStream events.
        hr = SelectStreams(pPD, pOp->Data());
    }

    if (SUCCEEDED(hr))
    {
        m_state = STATE_STARTED;

        // Queue the "started" event. The event data is the start position.
        hr = m_pEventQueue->QueueEventParamVar(
            MESourceStarted,
            GUID_NULL,
            S_OK,
            &pOp->Data()
            );
    }

    if (FAILED(hr))
    {
        // Failure. Send the error code to the application.

        // Note: It's possible that QueueEvent itself failed, in which case it
        // is likely to fail again. But there is no good way to recover in
        // that case.

        (void)m_pEventQueue->QueueEventParamVar(
            MESourceStarted, GUID_NULL, hr, NULL);
    }

    CompleteAsyncOp(pOp);

    SafeRelease(&pEvent);
    SafeRelease(&pPD);
    return hr;
}

暫停

IMFMediaSource::P ause方法會暫停媒體來源。 實作此方法,如下所示:

  1. 將非同步作業排入佇列。
  2. 每個作用中的資料流程都會傳送 MEStreamPaused 事件。
  3. 媒體來源會傳送 MESourcePaused 事件。

暫停時,來源會排入範例要求佇列,而不會進行處理。 (請參閱 範例 Requests.)

Stop

IMFMediaSource::Stop方法會停止媒體來源。 實作此方法,如下所示:

  1. 將非同步作業排入佇列。
  2. 每個作用中的資料流程都會傳送 MEStreamStopped 事件。
  3. 清除所有已排入佇列的範例和範例要求。
  4. 媒體來源會傳送 MESourceStopped 事件。

停止時,來源會拒絕樣本的所有要求。

如果在進行 I/O 要求時停止來源,則來源進入停止狀態之後,I/O 要求可能會完成。 在此情況下,來源應該捨棄該 I/O 要求的結果。

範例要求

媒體基礎會使用 提取 模型,其中管線會從媒體來源要求範例。 這與 DirectShow 所使用的模型不同,其中來源會「推送」範例。

若要要求新的範例,媒體基礎管線會呼叫 IMFMediaStream::RequestSample。 這個方法會採用代表Token物件的IUnknown指標。 Token 物件的實作由呼叫端決定;它只會提供方法來讓呼叫端追蹤範例要求。 Token 參數也可以是 Null

假設來源使用非同步 I/O 要求來讀取資料,則範例產生不會與範例要求同步處理。 若要同步處理範例要求與範例產生,媒體來源會執行下列動作:

  1. 要求權杖會放在佇列上。
  2. 產生範例時,它們會放在第二個佇列上。
  3. 媒體來源會從第一個佇列提取要求權杖,並從第二個佇列提取範例,以完成範例要求。
  4. 媒體來源會傳送 MEMediaSample 事件。 事件包含範例的指標,而範例包含標記的指標。

下圖顯示 MEMediaSample 事件、範例和要求權杖之間的關聯性。

顯示 memediasample 和指向 imfsample 的範例佇列的圖表;imfsample 和要求佇列指向 iunknown

範例 MPEG-1 來源會實作此程式,如下所示:

  1. RequestSample方法會將要求放在 FIFO 佇列上。
  2. 當 I/O 要求完成時,媒體來源會建立新的範例,並將其放在第二個 FIFO 佇列上。 (此佇列的大小上限,以防止來源事先太遠讀取。)
  3. 每當這兩個佇列至少有一個專案 (一個要求和一個範例) 時,媒體來源就會從要求佇列傳送第一個範例來完成第一個要求。
  4. 若要傳遞範例,資料流程物件 (不是來源物件) 傳送 MEMediaSample 事件。

此時,有三種可能性:

  • 範例佇列中有另一個範例,但沒有相符的要求。
  • 有要求,但沒有範例。
  • 這兩個佇列都是空的;沒有範例,沒有要求。

如果範例佇列是空的,來源會檢查資料流程結尾 (請參閱 資料流程結束) 。 否則,它會啟動另一個資料 I/O 要求。 如果此程式期間發生任何錯誤,資料流程會傳送 MEError 事件。

下列程式碼會實作 IMFMediaStream::RequestSample 方法:

HRESULT MPEG1Stream::RequestSample(IUnknown* pToken)
{
    HRESULT hr = S_OK;
    IMFMediaSource *pSource = NULL;

    // Hold the media source object's critical section.
    SourceLock lock(m_pSource);

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

    if (m_state == STATE_STOPPED)
    {
        hr = MF_E_INVALIDREQUEST;
        goto done;
    }

    if (!m_bActive)
    {
        // If the stream is not active, it should not get sample requests.
        hr = MF_E_INVALIDREQUEST;
        goto done;
    }

    if (m_bEOS && m_Samples.IsEmpty())
    {
        // This stream has already reached the end of the stream, and the
        // sample queue is empty.
        hr = MF_E_END_OF_STREAM;
        goto done;
    }

    hr = m_Requests.InsertBack(pToken);
    if (FAILED(hr))
    {
        goto done;
    }

    // Dispatch the request.
    hr = DispatchSamples();
    if (FAILED(hr))
    {
        goto done;
    }

done:
    if (FAILED(hr) && (m_state != STATE_SHUTDOWN))
    {
        // An error occurred. Send an MEError even from the source,
        // unless the source is already shut down.
        hr = m_pSource->QueueEvent(MEError, GUID_NULL, hr, NULL);
    }
    return hr;
}

方法 DispatchSamples 會從範例佇列提取範例、將它們與擱置的範例要求相符,並將 MEMediaSample 事件排入佇列:

HRESULT MPEG1Stream::DispatchSamples()
{
    HRESULT hr = S_OK;
    BOOL bNeedData = FALSE;
    BOOL bEOS = FALSE;

    SourceLock lock(m_pSource);

    // An I/O request can complete after the source is paused, stopped, or
    // shut down. Do not deliver samples unless the source is running.
    if (m_state != STATE_STARTED)
    {
        return S_OK;
    }

    IMFSample *pSample = NULL;
    IUnknown  *pToken = NULL;

    // Deliver as many samples as we can.
    while (!m_Samples.IsEmpty() && !m_Requests.IsEmpty())
    {
        // Pull the next sample from the queue.
        hr = m_Samples.RemoveFront(&pSample);
        if (FAILED(hr))
        {
            goto done;
        }

        // Pull the next request token from the queue. Tokens can be NULL.
        hr = m_Requests.RemoveFront(&pToken);
        if (FAILED(hr))
        {
            goto done;
        }

        if (pToken)
        {
            // Set the token on the sample.
            hr = pSample->SetUnknown(MFSampleExtension_Token, pToken);
            if (FAILED(hr))
            {
                goto done;
            }
        }

        // Send an MEMediaSample event with the sample.
        hr = m_pEventQueue->QueueEventParamUnk(
            MEMediaSample, GUID_NULL, S_OK, pSample);

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

        SafeRelease(&pSample);
        SafeRelease(&pToken);
    }

    if (m_Samples.IsEmpty() && m_bEOS)
    {
        // The sample queue is empty AND we have reached the end of the source
        // stream. Notify the pipeline by sending the end-of-stream event.

        hr = m_pEventQueue->QueueEventParamVar(
            MEEndOfStream, GUID_NULL, S_OK, NULL);

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

        // Notify the source. It will send the end-of-presentation event.
        hr = m_pSource->QueueAsyncOperation(SourceOp::OP_END_OF_STREAM);
        if (FAILED(hr))
        {
            goto done;
        }
    }
    else if (NeedsData())
    {
        // The sample queue is empty; the request queue is not empty; and we
        // have not reached the end of the stream. Ask for more data.
        hr = m_pSource->QueueAsyncOperation(SourceOp::OP_REQUEST_DATA);
        if (FAILED(hr))
        {
            goto done;
        }
    }

done:
    if (FAILED(hr) && (m_state != STATE_SHUTDOWN))
    {
        // An error occurred. Send an MEError even from the source,
        // unless the source is already shut down.
        m_pSource->QueueEvent(MEError, GUID_NULL, hr, NULL);
    }

    SafeRelease(&pSample);
    SafeRelease(&pToken);
    return S_OK;
}

DispatchSamples 下列情況下會呼叫 方法:

  • 在 RequestSample方法內。
  • 當媒體來源從暫停狀態重新開機時。
  • I/O 要求完成時。

資料流程結束

當資料流程沒有更多資料,且已傳遞該資料流程的所有範例時,資料流程物件會傳送 MEEndOfStream 事件。

當所有作用中的資料流程都完成時,媒體來源會傳送 MEEndOfPresentation 事件。

非同步作業

撰寫媒體來源的最困難部分可能是瞭解媒體基礎非同步模型。

控制串流之媒體來源上的所有方法都是非同步。 在每個案例中,方法會執行一些初始驗證,例如檢查參數。 然後,來源會將其餘工作分派至工作佇列。 作業完成之後,媒體來源會透過媒體來源的 IMFMediaEventGenerator 介面,將事件傳回給呼叫端。 因此,請務必瞭解工作佇列。

若要將專案放在工作佇列上,您可以呼叫 MFPutWorkItemMFPutWorkItemEx。 MPEG-1 來源會使用 MFPutWorkItem,但兩個函式會執行相同的動作。 MFPutWorkItem函式採用下列參數:

  • 識別工作佇列的 DWORD 值。 您可以建立私人工作佇列或使用 MFASYNC_CALLBACK_QUEUE_STANDARD
  • IMFAsyncCallback介面的指標。 系統會叫用這個回呼介面來執行工作。
  • 選擇性狀態物件,必須實作 IUnknown

工作佇列是由一或多個背景工作執行緒提供服務,這些執行緒會持續從佇列提取下一個工作專案,並叫用回呼介面的 IMFAsyncCallback::Invoke 方法。

工作專案不保證會依照您在佇列上放置工作專案的循序執行。 請記住,多個執行緒可以服務相同的工作佇列,因此 用呼叫可能會重迭或順序錯亂。 因此,媒體來源會依正確的順序提交工作佇列專案,以維持正確的內部狀態。 只有在上一個作業完成時,來源才會啟動下一個作業。

為了表示擱置的作業,MPEG-1 來源會定義名為 的 SourceOp 類別:

// Represents a request for an asynchronous operation.

class SourceOp : public IUnknown
{
public:

    enum Operation
    {
        OP_START,
        OP_PAUSE,
        OP_STOP,
        OP_REQUEST_DATA,
        OP_END_OF_STREAM
    };

    static HRESULT CreateOp(Operation op, SourceOp **ppOp);
    static HRESULT CreateStartOp(IMFPresentationDescriptor *pPD, SourceOp **ppOp);

    // IUnknown
    STDMETHODIMP QueryInterface(REFIID iid, void** ppv);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();

    SourceOp(Operation op);
    virtual ~SourceOp();

    HRESULT SetData(const PROPVARIANT& var);

    Operation Op() const { return m_op; }
    const PROPVARIANT& Data() { return m_data;}

protected:
    long        m_cRef;     // Reference count.
    Operation   m_op;
    PROPVARIANT m_data;     // Data for the operation.
};

列舉 Operation 會識別擱置中的作業。 類別也包含 PROPVARIANT ,用來傳達作業的任何其他資料。

作業佇列

為了序列化作業,媒體來源會維護物件的佇列 SourceOp 。 它會使用協助程式類別來管理佇列:

template <class OP_TYPE>
class OpQueue : public IUnknown
{
public:

    typedef ComPtrList<OP_TYPE>   OpList;

    HRESULT QueueOperation(OP_TYPE *pOp);

protected:

    HRESULT ProcessQueue();
    HRESULT ProcessQueueAsync(IMFAsyncResult *pResult);

    virtual HRESULT DispatchOperation(OP_TYPE *pOp) = 0;
    virtual HRESULT ValidateOperation(OP_TYPE *pOp) = 0;

    OpQueue(CRITICAL_SECTION& critsec)
        : m_OnProcessQueue(this, &OpQueue::ProcessQueueAsync),
          m_critsec(critsec)
    {
    }

    virtual ~OpQueue()
    {
    }

protected:
    OpList                  m_OpQueue;         // Queue of operations.
    CRITICAL_SECTION&       m_critsec;         // Protects the queue state.
    AsyncCallback<OpQueue>  m_OnProcessQueue;  // ProcessQueueAsync callback.
};

類別 OpQueue 的設計目的是要由執行非同步工作專案的元件繼承。 OP_TYPE樣板參數是用來代表佇列中工作專案的物件類型,在此案例中,OP_TYPESourceOp 。 類別 OpQueue 會實作下列方法:

  • QueueOperation 將新專案放在佇列上。
  • ProcessQueue 從佇列分派下一個作業。 這個方法是非同步方法。
  • ProcessQueueAsync 完成非同步 ProcessQueue 方法。

衍生類別必須實作另一個兩種方法:

  • ValidateOperation 會根據媒體來源的目前狀態,檢查是否有效執行指定的作業。
  • DispatchOperation 會執行非同步工作專案。

作業佇列的使用方式如下:

  1. 媒體基礎管線會在媒體來源上呼叫非同步方法,例如 IMFMediaSource::Start
  2. 非同步方法會呼叫 QueueOperation ,它會將Start作業放在佇列上,並以物件) 的形式 SourceOp 呼叫 ProcessQueue (。
  3. ProcessQueue 會呼叫 MFPutWorkItem
  4. 工作佇列執行緒會呼叫 ProcessQueueAsync
  5. 方法 ProcessQueueAsync 會呼叫 ValidateOperationDispatchOperation

下列程式碼會將 MPEG-1 來源上的新作業排入佇列:

template <class OP_TYPE>
HRESULT OpQueue<OP_TYPE>::QueueOperation(OP_TYPE *pOp)
{
    HRESULT hr = S_OK;

    EnterCriticalSection(&m_critsec);

    hr = m_OpQueue.InsertBack(pOp);
    if (SUCCEEDED(hr))
    {
        hr = ProcessQueue();
    }

    LeaveCriticalSection(&m_critsec);
    return hr;
}

下列程式碼會處理佇列:

template <class OP_TYPE>
HRESULT OpQueue<OP_TYPE>::ProcessQueue()
{
    HRESULT hr = S_OK;
    if (m_OpQueue.GetCount() > 0)
    {
        hr = MFPutWorkItem(
            MFASYNC_CALLBACK_QUEUE_STANDARD,    // Use the standard work queue.
            &m_OnProcessQueue,                  // Callback method.
            NULL                                // State object.
            );
    }
    return hr;
}

方法 ValidateOperation 會檢查 MPEG-1 來源是否可以分派佇列中的下一個作業。 如果另一個作業正在進行中, ValidateOperation 則傳回 MF_E_NOTACCEPTING。 這可確保 DispatchOperation 在有另一個作業擱置時,不會呼叫 。

HRESULT MPEG1Source::ValidateOperation(SourceOp *pOp)
{
    if (m_pCurrentOp != NULL)
    {
        return MF_E_NOTACCEPTING;
    }
    return S_OK;
}

DispatchOperation 方法會在作業類型上切換:

//-------------------------------------------------------------------
// DispatchOperation
//
// Performs the asynchronous operation indicated by pOp.
//
// NOTE:
// This method implements the pure-virtual OpQueue::DispatchOperation
// method. It is always called from a work-queue thread.
//-------------------------------------------------------------------

HRESULT MPEG1Source::DispatchOperation(SourceOp *pOp)
{
    EnterCriticalSection(&m_critSec);

    HRESULT hr = S_OK;

    if (m_state == STATE_SHUTDOWN)
    {
        LeaveCriticalSection(&m_critSec);

        return S_OK; // Already shut down, ignore the request.
    }

    switch (pOp->Op())
    {

    // IMFMediaSource methods:

    case SourceOp::OP_START:
        hr = DoStart((StartOp*)pOp);
        break;

    case SourceOp::OP_STOP:
        hr = DoStop(pOp);
        break;

    case SourceOp::OP_PAUSE:
        hr = DoPause(pOp);
        break;

    // Operations requested by the streams:

    case SourceOp::OP_REQUEST_DATA:
        hr = OnStreamRequestSample(pOp);
        break;

    case SourceOp::OP_END_OF_STREAM:
        hr = OnEndOfStream(pOp);
        break;

    default:
        hr = E_UNEXPECTED;
    }

    if (FAILED(hr))
    {
        StreamingError(hr);
    }

    LeaveCriticalSection(&m_critSec);
    return hr;
}

總括來說:

  1. 管線會呼叫非同步方法,例如 IMFMediaSource::Start
  2. 非同步方法會呼叫 OpQueue::QueueOperation ,傳入 物件的指標 SourceOp
  3. 方法會將 QueueOperation 作業放在 m_OpQueue 佇列上,並呼叫 OpQueue::ProcessQueue
  4. 方法 ProcessQueue 會呼叫 MFPutWorkItem。 此時,所有專案都會在 Media Foundation 工作佇列執行緒上發生。 非同步方法會傳回給呼叫端。
  5. 工作佇列執行緒會呼叫 OpQueue::ProcessQueueAsync 方法。
  6. 方法 ProcessQueueAsync 會呼叫 MPEG1Source:ValidateOperation 以驗證作業。
  7. 方法 ProcessQueueAsync 會呼叫 MPEG1Source::DispatchOperation 來處理作業。

此設計有數個優點:

  • 方法是非同步,因此它們不會封鎖呼叫應用程式的執行緒。
  • 作業會分派在媒體基礎工作佇列執行緒上,此執行緒會在管線元件之間共用。 因此,媒體來源不會建立自己的執行緒,減少所建立的執行緒總數。
  • 媒體來源不會在等候作業完成時封鎖。 這可減少媒體來源意外造成死結的機會,並有助於減少內容切換。
  • 媒體來源可以使用非同步 I/O 來讀取來源檔案 (,方法是呼叫 IMFByteStream::BeginRead) 。 媒體來源不需要在等候 I/O 常式完成時封鎖。

如果您遵循 SDK 範例中顯示的模式,您可以專注于媒體來源的特定詳細資料。

媒體來源

撰寫自訂媒體來源