使用源读取器处理媒体数据

本主题介绍如何使用 源读取器 处理媒体数据。

若要使用源读取器,请遵循以下基本步骤:

  1. 创建源读取器的实例。
  2. 枚举可能的输出格式。
  3. 设置每个流的实际输出格式。
  4. 处理源中的数据。

本主题的其余部分详细介绍了这些步骤。

创建源读取器

若要创建源读取器的实例,请调用以下函数之一:

函数 说明
MFCreateSourceReaderFromURL
将 URL 作为输入。 此函数使用 源解析程序 从 URL 创建媒体源。
MFCreateSourceReaderFromByteStream
获取指向字节流的指针。 此函数还使用源解析程序创建媒体源。
MFCreateSourceReaderFromMediaSource
获取指向已创建的媒体源的指针。 此函数适用于源解析程序无法创建的媒体源,例如捕获设备或自定义媒体源。

 

通常,对于媒体文件,使用 MFCreateSourceReaderFromURL。 对于网络摄像头等设备,请使用 MFCreateSourceReaderFromMediaSource。 (有关 Microsoft Media Foundation 中捕获设备的详细信息,请参阅 Audio/Video Capture.)

每个函数都采用可选的 IMFAttributes 指针,该指针用于设置源读取器上的各种选项,如这些函数的参考主题中所述。 若要获取默认行为,请将此参数设置为 NULL。 每个函数返回一个 IMFSourceReader 指针作为输出参数。 在调用其中任何函数之前,必须调用 CoInitialize (Ex) MFStartup 函数。

以下代码从 URL 创建源读取器。

int __cdecl wmain(int argc, __in_ecount(argc) PCWSTR* argv)
{
    if (argc < 2)
    {
        return 1;
    }

    const WCHAR *pszURL = argv[1];

    // Initialize the COM runtime.
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (SUCCEEDED(hr))
    {
        // Initialize the Media Foundation platform.
        hr = MFStartup(MF_VERSION);
        if (SUCCEEDED(hr))
        {
            // Create the source reader.
            IMFSourceReader *pReader;
            hr = MFCreateSourceReaderFromURL(pszURL, NULL, &pReader);
            if (SUCCEEDED(hr))
            {
                ReadMediaFile(pReader);
                pReader->Release();
            }
            // Shut down Media Foundation.
            MFShutdown();
        }
        CoUninitialize();
    }
}

枚举输出格式

每个媒体源至少有一个流。 例如,视频文件可能包含视频流和音频流。 每个流的格式都使用由 IMFMediaType 接口表示的媒体类型进行描述。 有关媒体类型的详细信息,请参阅 媒体类型。 必须检查媒体类型以了解从源读取器获取的数据的格式。

最初,每个流都有一个默认格式,可以通过调用 IMFSourceReader::GetCurrentMediaType 方法找到该格式:

对于每个流,媒体源都会为该流提供可能媒体类型的列表。 类型数取决于源。 如果源表示媒体文件,则每个流通常只有一种类型。 另一方面,网络摄像头可能能够以多种不同的格式流式传输视频。 在这种情况下,应用程序可以从媒体类型列表中选择要使用的格式。

若要获取流的媒体类型之一,请调用 IMFSourceReader::GetNativeMediaType 方法。 此方法采用两个索引参数:流的索引,以及流媒体类型列表中的索引。 若要枚举流的所有类型,请在保持流索引不变的同时递增列表索引。 当列表索引超出边界时, GetNativeMediaType 将返回 MF_E_NO_MORE_TYPES

HRESULT EnumerateTypesForStream(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
    HRESULT hr = S_OK;
    DWORD dwMediaTypeIndex = 0;

    while (SUCCEEDED(hr))
    {
        IMFMediaType *pType = NULL;
        hr = pReader->GetNativeMediaType(dwStreamIndex, dwMediaTypeIndex, &pType);
        if (hr == MF_E_NO_MORE_TYPES)
        {
            hr = S_OK;
            break;
        }
        else if (SUCCEEDED(hr))
        {
            // Examine the media type. (Not shown.)

            pType->Release();
        }
        ++dwMediaTypeIndex;
    }
    return hr;
}

若要枚举每个流的媒体类型,请递增流索引。 当流索引超出边界时, GetNativeMediaType 将返回 MF_E_INVALIDSTREAMNUMBER

HRESULT EnumerateMediaTypes(IMFSourceReader *pReader)
{
    HRESULT hr = S_OK;
    DWORD dwStreamIndex = 0;

    while (SUCCEEDED(hr))
    {
        hr = EnumerateTypesForStream(pReader, dwStreamIndex);
        if (hr == MF_E_INVALIDSTREAMNUMBER)
        {
            hr = S_OK;
            break;
        }
        ++dwStreamIndex;
    }
    return hr;
}

设置输出格式

若要更改输出格式,请调用 IMFSourceReader::SetCurrentMediaType 方法。 此方法采用流索引和媒体类型:

hr = pReader->SetCurrentMediaType(dwStreamIndex, pMediaType);

对于媒体类型,这取决于是否要插入解码器。

  • 若要直接从源获取数据而不对其进行解码,请使用 GetNativeMediaType 返回的类型之一。
  • 若要解码流,请创建描述所需未压缩格式的新媒体类型。

对于解码器,请按如下所示创建媒体类型:

  1. 调用 MFCreateMediaType 创建新的媒体类型。
  2. 设置 MF_MT_MAJOR_TYPE 属性以指定音频或视频。
  3. 设置 MF_MT_SUBTYPE 属性以指定解码格式的子类型。 (请参阅 音频子类型 GUID视频子类型 GUIDs.)
  4. 调用 IMFSourceReader::SetCurrentMediaType

源读取器将自动加载解码器。 若要获取解码格式的完整详细信息,请在调用 SetCurrentMediaType 后调用 IMFSourceReader::GetCurrentMediaType

以下代码为 RGB-32 配置视频流,为 PCM 音频配置音频流。

HRESULT ConfigureDecoder(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
    IMFMediaType *pNativeType = NULL;
    IMFMediaType *pType = NULL;

    // Find the native format of the stream.
    HRESULT hr = pReader->GetNativeMediaType(dwStreamIndex, 0, &pNativeType);
    if (FAILED(hr))
    {
        return hr;
    }

    GUID majorType, subtype;

    // Find the major type.
    hr = pNativeType->GetGUID(MF_MT_MAJOR_TYPE, &majorType);
    if (FAILED(hr))
    {
        goto done;
    }

    // Define the output type.
    hr = MFCreateMediaType(&pType);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pType->SetGUID(MF_MT_MAJOR_TYPE, majorType);
    if (FAILED(hr))
    {
        goto done;
    }

    // Select a subtype.
    if (majorType == MFMediaType_Video)
    {
        subtype= MFVideoFormat_RGB32;
    }
    else if (majorType == MFMediaType_Audio)
    {
        subtype = MFAudioFormat_PCM;
    }
    else
    {
        // Unrecognized type. Skip.
        goto done;
    }

    hr = pType->SetGUID(MF_MT_SUBTYPE, subtype);
    if (FAILED(hr))
    {
        goto done;
    }

    // Set the uncompressed format.
    hr = pReader->SetCurrentMediaType(dwStreamIndex, NULL, pType);
    if (FAILED(hr))
    {
        goto done;
    }

done:
    SafeRelease(&pNativeType);
    SafeRelease(&pType);
    return hr;
}

处理媒体数据

若要从源获取媒体数据,请调用 IMFSourceReader::ReadSample 方法,如以下代码所示。

        DWORD streamIndex, flags;
        LONGLONG llTimeStamp;

        hr = pReader->ReadSample(
            MF_SOURCE_READER_ANY_STREAM,    // Stream index.
            0,                              // Flags.
            &streamIndex,                   // Receives the actual stream index. 
            &flags,                         // Receives status flags.
            &llTimeStamp,                   // Receives the time stamp.
            &pSample                        // Receives the sample or NULL.
            );

第一个参数是要获取其数据的流的索引。 还可以指定 MF_SOURCE_READER_ANY_STREAM 从任何流获取下一个可用数据。 第二个参数包含可选标志;有关 这些 列表,请参阅MF_SOURCE_READER_CONTROL_FLAG。 第三个参数接收实际生成数据的流的索引。 如果将第一个参数设置为 MF_SOURCE_READER_ANY_STREAM,则需要此信息。 第四个参数接收状态标志,指示读取数据时可能发生的各种事件,例如流中的格式更改。 有关状态标志的列表,请参阅 MF_SOURCE_READER_FLAG

如果媒体源能够为请求的流生成数据, ReadSample 的最后一个参数会收到指向媒体示例对象的 IMFSample 接口的指针。 使用媒体示例可以:

  • 获取指向媒体数据的指针。
  • 获取演示时间和示例持续时间。
  • 获取描述交错、场占位和示例其他方面的属性。

媒体数据的内容取决于流的格式。 对于未压缩的视频流,每个媒体示例都包含一个视频帧。 对于未压缩的音频流,每个媒体示例都包含一系列音频帧。

ReadSample 方法可以返回S_OK但不返回 pSample 参数中的媒体示例。 例如,当到达文件的末尾时,ReadSample 会将 dwFlags 中的MF_SOURCE_READERF_ENDOFSTREAM标志设置为NULL。 在这种情况下, ReadSample 方法返回 S_OK ,因为未发生错误,即使 pSample 参数设置为 NULL 也是如此。 因此,在取消引用 pSample 之前,始终检查它的值。

以下代码演示如何在循环中调用 ReadSample,并检查 方法返回的信息,直到到达媒体文件的末尾。

HRESULT ProcessSamples(IMFSourceReader *pReader)
{
    HRESULT hr = S_OK;
    IMFSample *pSample = NULL;
    size_t  cSamples = 0;

    bool quit = false;
    while (!quit)
    {
        DWORD streamIndex, flags;
        LONGLONG llTimeStamp;

        hr = pReader->ReadSample(
            MF_SOURCE_READER_ANY_STREAM,    // Stream index.
            0,                              // Flags.
            &streamIndex,                   // Receives the actual stream index. 
            &flags,                         // Receives status flags.
            &llTimeStamp,                   // Receives the time stamp.
            &pSample                        // Receives the sample or NULL.
            );

        if (FAILED(hr))
        {
            break;
        }

        wprintf(L"Stream %d (%I64d)\n", streamIndex, llTimeStamp);
        if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
        {
            wprintf(L"\tEnd of stream\n");
            quit = true;
        }
        if (flags & MF_SOURCE_READERF_NEWSTREAM)
        {
            wprintf(L"\tNew stream\n");
        }
        if (flags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED)
        {
            wprintf(L"\tNative type changed\n");
        }
        if (flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
        {
            wprintf(L"\tCurrent type changed\n");
        }
        if (flags & MF_SOURCE_READERF_STREAMTICK)
        {
            wprintf(L"\tStream tick\n");
        }

        if (flags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED)
        {
            // The format changed. Reconfigure the decoder.
            hr = ConfigureDecoder(pReader, streamIndex);
            if (FAILED(hr))
            {
                break;
            }
        }

        if (pSample)
        {
            ++cSamples;
        }

        SafeRelease(&pSample);
    }

    if (FAILED(hr))
    {
        wprintf(L"ProcessSamples FAILED, hr = 0x%x\n", hr);
    }
    else
    {
        wprintf(L"Processed %d samples\n", cSamples);
    }
    SafeRelease(&pSample);
    return hr;
}

清空数据管道

在数据处理期间,解码器或其他转换可能会缓冲输入样本。 在下图中,应用程序调用 ReadSample 并接收演示时间等于 t1 的示例。 解码器保存 t2t3 的样本。

显示解码器中缓冲的插图。

在下一次调用 ReadSample 时,源读取器可能会将 t4 提供给解码器,并将 t2 返回到应用程序。

如果要解码当前在解码器中缓冲的所有样本,而不将任何新样本传递给解码器,请在 ReadSampledwControlFlags 参数中设置MF_SOURCE_READER_CONTROLF_DRAIN标志。 继续在循环中执行此操作,直到 ReadSample 返回 NULL 示例指针。 根据解码器缓冲样本的方式,这种情况可能会立即发生或在多次调用 ReadSample 之后发生。

获取文件持续时间

若要获取媒体文件的持续时间,请调用 IMFSourceReader::GetPresentationAttribute 方法并请求 MF_PD_DURATION 属性,如以下代码所示。

HRESULT GetDuration(IMFSourceReader *pReader, LONGLONG *phnsDuration)
{
    PROPVARIANT var;
    HRESULT hr = pReader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, 
        MF_PD_DURATION, &var);
    if (SUCCEEDED(hr))
    {
        hr = PropVariantToInt64(var, phnsDuration);
        PropVariantClear(&var);
    }
    return hr;
}

此处显示的函数获取持续时间(以 100 纳秒为单位)。 除以 10,000,000 可得到持续时间(以秒为单位)。

寻求

从本地文件获取数据的媒体源通常可以查找文件中的任意位置。 捕获设备(如网络摄像头)通常无法查找,因为数据是实时的。 通过网络流式传输数据的源可能能够查找,具体取决于网络流式处理协议。

若要了解媒体源是否可以查找,请调用 IMFSourceReader::GetPresentationAttribute 并请求 MF_SOURCE_READER_MEDIASOURCE_CHARACTERISTICS 属性,如以下代码所示:

HRESULT GetSourceFlags(IMFSourceReader *pReader, ULONG *pulFlags)
{
    ULONG flags = 0;

    PROPVARIANT var;
    PropVariantInit(&var);

    HRESULT hr = pReader->GetPresentationAttribute(
        MF_SOURCE_READER_MEDIASOURCE, 
        MF_SOURCE_READER_MEDIASOURCE_CHARACTERISTICS, 
        &var);

    if (SUCCEEDED(hr))
    {
        hr = PropVariantToUInt32(var, &flags);
    }
    if (SUCCEEDED(hr))
    {
        *pulFlags = flags;
    }

    PropVariantClear(&var);
    return hr;
}

此函数从源获取一组功能标志。 这些标志在 MFMEDIASOURCE_CHARACTERISTICS 枚举中定义。 两个标志与查找相关:

标志 描述
MFMEDIASOURCE_CAN_SEEK
源可以查找。
MFMEDIASOURCE_HAS_SLOW_SEEK
查找可能需要很长时间才能完成。 例如,源可能需要先下载整个文件,然后才能查找。 (源没有严格的条件可返回此标志。)

 

以下代码测试 MFMEDIASOURCE_CAN_SEEK 标志。

BOOL SourceCanSeek(IMFSourceReader *pReader)
{
    BOOL bCanSeek = FALSE;
    ULONG flags;
    if (SUCCEEDED(GetSourceFlags(pReader, &flags)))
    {
        bCanSeek = ((flags & MFMEDIASOURCE_CAN_SEEK) == MFMEDIASOURCE_CAN_SEEK);
    }
    return bCanSeek;
}

若要查找,请调用 IMFSourceReader::SetCurrentPosition 方法,如以下代码所示。

HRESULT SetPosition(IMFSourceReader *pReader, const LONGLONG& hnsPosition)
{
    PROPVARIANT var;
    HRESULT hr = InitPropVariantFromInt64(hnsPosition, &var);
    if (SUCCEEDED(hr))
    {
        hr = pReader->SetCurrentPosition(GUID_NULL, var);
        PropVariantClear(&var);
    }
    return hr;
}

第一个参数提供用于指定搜寻位置的时间格式。 Media Foundation 中的所有媒体源都需要支持 100 纳秒单位,由 值GUID_NULL指示。 第二个参数是包含搜寻位置的 PROPVARIANT 。 对于 100 纳秒的时间单位,数据类型为 LONGLONG

请注意,并非每个媒体源都提供帧准确的查找。 查找的准确性取决于多个因素,例如关键帧间隔、媒体文件是否包含索引,以及数据是否具有常量或可变比特率。 因此,在查找文件中的位置后,无法保证下一个示例中的时间戳与请求的位置完全匹配。 通常,实际位置不会晚于请求的位置,因此可以丢弃样本,直到到达流中的所需点。

播放速率

尽管可以使用源阅读器设置播放速率,但这样做通常不是很有用,原因如下:

  • 源阅读器不支持反向播放,即使媒体源支持反向播放。
  • 应用程序控制呈现时间,因此应用程序可以实现快速或慢速播放,而无需在源上设置速率。
  • 某些媒体源支持 精简 模式,其中源提供的样本较少,通常只是关键帧。 但是,如果要删除非关键帧,则可以检查MFSampleExtension_CleanPoint属性的每个示例。

若要使用源读取器设置播放速率,请调用 IMFSourceReader::GetServiceForStream 方法,从媒体源获取 IMFRateSupportIMFRateControl 接口。

硬件加速

源阅读器与 Microsoft DirectX 视频加速 (DXVA) 2.0 兼容,用于硬件加速视频解码。 若要将 DXVA 与源读取器配合使用,请执行以下步骤。

  1. 创建 Microsoft Direct3D 设备。
  2. 调用 DXVA2CreateDirect3DDeviceManager9 函数以创建 Direct3D 设备管理器。 此函数获取指向 IDirect3DDeviceManager9 接口的 指针。
  3. 使用指向 Direct3D 设备的指针调用 IDirect3DDeviceManager9::ResetDevice 方法。
  4. 通过调用 MFCreateAttributes 函数创建属性存储。
  5. 创建源读取器。 在创建函数的 pAttributes 参数中传递属性存储。

提供 Direct3D 设备时,源读取器会分配与 DXVA 视频处理器 API 兼容的视频样本。 可以使用 DXVA 视频处理来执行硬件去交错或视频混合。 有关详细信息,请参阅 DXVA 视频处理。 此外,如果解码器支持 DXVA 2.0,它将使用 Direct3D 设备执行硬件加速解码。

重要

从 Windows 8 开始,可以使用 IMFDXGIDeviceManager 代替 IDirect3DDeviceManager9。 对于 Windows 应用商店应用,必须使用 IMFDXGIDeviceManager。 有关详细信息,请参阅 Direct3D 11 视频 API

 

源读取者