案例研究:MPEG-1 媒体源

在 Microsoft Media Foundation 中,将媒体数据引入数据管道的对象称为 媒体源。 本主题深入探讨 MPEG-1 媒体源 SDK 示例。

先决条件

在阅读本主题之前,应了解以下媒体基础概念:

还应基本了解媒体基础体系结构,特别是媒体源在管道中的角色。 (有关详细信息,请参阅 Media Sources.)

此外,你可能想要阅读主题 编写自定义媒体源,其中更笼统地概述了此处所述的步骤。

本主题不会重现 SDK 示例中的所有代码,因为该示例相当大。

MPEG-1 源中使用的 C++ 类

示例 MPEG-1 源使用以下 C++ 类实现:

  • MPEG1ByteStreamHandler. 实现媒体源的字节流处理程序。 给定字节流,字节流处理程序会创建源的实例。
  • MPEG1Source. 实现媒体源。
  • MPEG1Stream. 实现媒体流对象。 媒体源为 MPEG-1 位流中的每个音频或视频流创建一个 MPEG1Stream 对象。
  • Parser. 分析 MPEG-1 位流。 在大多数情况下,此类的详细信息与 Media Foundation API 无关。
  • SourceOp、OpQueue:这两个类管理媒体源中的异步操作。 (请参阅 异步操作) 。

本主题稍后将介绍其他帮助程序类。

Byte-Stream处理程序

字节流处理程序是创建媒体源的对象。 字节流处理程序由源解析程序创建;应用程序不直接与字节流处理程序交互。 源解析程序通过查找注册表来发现字节流处理程序。 处理程序按文件扩展名或 MIME 类型注册。 对于 MPEG-1 源,为“.mpg”文件扩展名注册字节流处理程序。

注意

如果要支持自定义 URL 方案,还可以编写 方案处理程序。 MPEG-1 源专为本地文件设计,Media Foundation 已经为“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 序列标头。 然后,它创建 一个演示文稿描述符,该描述符是用于描述文件中音频和视频流的对象。 (有关详细信息,请参阅 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 文件的内容,包括以下信息:

  • 流的数。
  • 每个流的格式。
  • 流标识符。
  • 每个流的选择状态 (选择或取消选择) 。

就媒体基础体系结构而言,演示文稿描述符包含一个或多个 流描述符。 每个流描述符都包含一个 媒体类型处理程序,该处理程序用于获取或设置流上的媒体类型。 Media Foundation 为演示文稿描述符和流描述符提供库存实现;这些适用于大多数媒体源。

若要创建演示文稿描述符,请执行以下步骤:

  1. 对于每个流:
    1. 提供流 ID 和可能的媒体类型的数组。 如果流支持多个媒体类型,请按首选项(如果有)对媒体类型列表进行排序。 (将最佳类型放在第一位,将最差类型放在最后。)
    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 属性包含源的总持续时间。 如果提前知道持续时间,请设置此属性;例如,持续时间可能在文件标头中指定,具体取决于文件格式。 应用程序可能会显示此值,或者使用它来设置进度栏或查找栏。

流式处理状态

媒体源定义以下状态:

状态 说明
Started 源接受并处理示例请求。
已暂停 源接受示例请求,但不处理它们。 请求将排入队列,直到源启动。
已停止。 源拒绝示例请求。

 

开始

IMFMediaSource::Start 方法启动媒体源。 它采用了以下参数:

  • 演示文稿描述符。
  • 时间格式 GUID。
  • 开始位置。

应用程序必须通过在源上调用 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::P ause 方法暂停媒体源。 按如下所示实现此方法:

  1. 将异步操作排队。
  2. 每个活动流发送 一个 MEStreamPaused 事件。
  3. 媒体源发送 MESourcePaused 事件。

暂停时,源会将示例请求排在队列中,而不对其进行处理。 (请参阅 示例 Requests.)

停止

IMFMediaSource::Stop 方法停止媒体源。 按如下所示实现此方法:

  1. 将异步操作排队。
  2. 每个活动流发送 MEStreamStopped 事件。
  3. 清除所有排队的样本和示例请求。
  4. 媒体源发送 MESourceStopped 事件。

停止时,源会拒绝所有样本请求。

如果在 I/O 请求正在进行时停止源,则 I/O 请求可能会在源进入停止状态后完成。 在这种情况下,源应放弃该 I/O 请求的结果。

示例请求

媒体基础使用 拉取 模型,其中管道从媒体源请求示例。 这与 DirectShow 使用的模型不同,后者中的源会“推送”样本。

为了请求新示例,媒体基础管道调用 IMFMediaStream::RequestSample。 此方法采用表示令牌对象的 IUnknown 指针。 令牌对象的实现由调用方决定;它只是为调用方提供了一种跟踪示例请求的方法。 令牌参数也可以为 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_TYPESourceOp。 类 OpQueue 实现以下方法:

  • 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 以处理操作。

此设计有几个优点:

  • 方法是异步的,因此它们不会阻止调用应用程序的线程。
  • 操作在媒体基础工作队列线程上调度,该线程在管道组件之间共享。 因此,媒体源不会创建自己的线程,从而减少创建的线程总数。
  • 等待操作完成时,媒体源不会阻止。 这降低了媒体源意外导致死锁的可能性,并有助于减少上下文切换。
  • 媒体源可以通过调用 IMFByteStream::BeginRead) ,使用异步 I/O (读取源文件。 等待 I/O 例程完成时,媒体源不需要阻止。

如果遵循 SDK 示例中所示的模式,则可以专注于媒体源的特定详细信息。

媒体源

编写自定义媒体源