共用方式為


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

在 Microsoft Media Foundation 中,將媒體數據導入數據管線的對象稱為媒體來源 。 本主題深入探討 MPEG-1 媒體來源 SDK 範例。

先決條件

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

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

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

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

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

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

  • MPEG1ByteStreamHandler。 實作媒體來源的位元組數據流處理程式。 指定位元組數據流時,位元組數據流處理程式會建立來源的實例。
  • MPEG1Source。 實作媒體來源。
  • MPEG1Stream。 實作媒體數據流物件。 媒體來源會為 MPEG-1 位數據流中的每個音訊或視訊數據流建立一個 MPEG1Stream 物件。
  • Parser。 剖析 MPEG-1 位數據流。 在大多數情況下,此類別的詳細數據與媒體基礎 API 無關。
  • SourceOp、OpQueue:這兩個類別負責管理媒體源中的異步操作。 (請參閱 異步操作)。

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

Byte-Stream 處理程式

位元串流處理器 控制器是用來創建媒體來源的物件。 位元組數據流處理程式是由來源解析程式所建立;應用程式不會直接與位元組數據流處理程序互動。 來源解析程式會藉由查看登錄來探索位元組數據流處理程式。 處理程式會依擴展名或MIME類型註冊。 針對 MPEG-1 來源,位元組數據流處理程式會註冊為 「.mpg」 擴展名。

注意

如果您想要支援自定義 URL 配置,您也可以在 撰寫設定處理程式。 MPEG-1 資源是專為本機檔案設計的,而媒體基礎架構已經為「file://」URL 提供了一個方案處理程式。

 

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

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

以下是 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 序列標頭。 然後,它會建立 簡報描述元,這是用來描述檔案中音訊和視訊數據流的物件。 (如需詳細資訊,請參閱 簡報描述元。)當 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. 提供數據流識別碼和可能媒體類型的陣列。 如果數據流支援多個媒體類型,請依喜好設定排序媒體類型清單,如果有的話。 (將最佳類型放在第一位,將最不理想的類型放在最後。)
    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 屬性包含來源的總持續時間。 如果您知道前方持續時間,請設定此屬性;例如,視檔格式而定,檔案標頭中可能會指定持續時間。 應用程式可能會顯示此值,或使用它來設定進度列或搜尋列。

串流狀態

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

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

 

開始

IMFMediaSource::Start 方法啟動媒體來源。 它採用下列參數:

  • 簡報描述符。
  • 時間格式的全域唯一識別碼。
  • 開始位置。

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

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

開始位置會指定為 PROPVARIANT,根據時間格式允許不同的數據類型。 對於 100-ns,PROPVARIANT 類型為 VT_I8VT_EMPTY。 如果 VT_I8,則 PROPVARIANT 包含以 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::Pause 方法會暫停媒體來源。 實作此方法,如下所示:

  1. 將非同步操作排入佇列。
  2. 每個作用中數據流都會傳送 MEStreamPaused 事件。
  3. 媒體來源會傳送 MESourcePaused 事件。

暫停時,來源會將範本請求至佇列,而不會進行處理。 (請參閱 範例要求

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

  1. 將異步作排入佇列。
  2. 每個作用中的數據流都會傳送一個 MEStreamStopped 事件。
  3. 清除所有已排入佇列的樣本和取樣請求。
  4. 媒體來源會傳送 MESourceStopped 事件。

停止時,來源會拒絕所有範例要求。

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

範例要求

Media Foundation 使用 拉取 模型,其中管線會向媒體來源請求樣本。 這與 DirectShow 所使用的模型不同,其中來源會「推送」範例。

若要要求新的範例,Media Foundation 管線會呼叫 IMFMediaStream::RequestSample。 此方法將接受一個 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 事件。
    • 事件數據是範例 IMFSample 介面的指標。
    • 如果要求包含令牌,請在範例上設定 MFSampleExtension_Token 屬性,將令牌附加至範例。

此時,有三種可能性:

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

如果樣本佇列是空的,來源會檢查流的結尾(請參閱 流結尾)。 否則,它會啟動另一個數據 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_TYPE 將會 SourceOpOpQueue 類別會實作下列方法:

  • QueueOperation 將新項目放入佇列中。
  • ProcessQueue 從佇列分派下一個作業。 這個方法是異步的。
  • ProcessQueueAsync 完成非同步 ProcessQueue 方法。

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

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

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

  1. 媒體基礎管線會在媒體來源上呼叫異步方法,例如 IMFMediaSource::Start
  2. 異步方法會呼叫 QueueOperation,它會將 Start 作業放在佇列上,並呼叫 ProcessQueue (以 SourceOp 物件的形式)。
  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。 從此刻起,一切都在媒體基礎架構的工作佇列執行緒上進行。 異步方法會將控制權傳回給呼叫端。
  5. 工作佇列線程會呼叫 OpQueue::ProcessQueueAsync 方法。
  6. ProcessQueueAsync 方法會呼叫 MPEG1Source:ValidateOperation 來驗證作業。
  7. ProcessQueueAsync 方法會呼叫 MPEG1Source::DispatchOperation 來處理作業。

此設計有幾個優點:

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

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

媒體來源

撰寫自定義媒體來源