如何编写 EVR 演示器

[此页中所述的组件增强的视频呈现器是一项旧功能。 它已由通过 MediaPlayerIMFMediaEngine 组件公开的简单视频呈现器 (SVR) 取代。 若要播放视频内容,应将数据发送到其中一个组件,并允许它们实例化新的视频呈现器。 这些组件已针对 Windows 10 和 Windows 11 进行了优化。 Microsoft 强烈建议新代码在 Windows 中尽可能使用 MediaPlayer 或较低级别的 IMFMediaEngine API 而不是 EVR 播放视频媒体。 如果可能,Microsoft 建议应重写使用旧 API 的现有代码,以尽可能地使用新的 API。]

本文介绍如何为增强的视频呈现器 (EVR) 编写自定义演示器。 自定义演示器可用于 DirectShow 和媒体基础;这两种技术的接口和对象模型是相同的,但是确切的操作顺序可能会有所不同。

本主题中的示例代码改编自 Windows SDK 中提供的 EVRPresenter 样本

本主题包含以下各节:

先决条件

在编写自定义演示器之前,应熟悉以下技术:

  • 增强的视频呈现器。 请参阅增强的视频呈现器
  • Direct3D 图形。 编写演示器并不需要了解 3D 图形,但必须知道如何创建 Direct3D 设备和管理 Direct3D 图面。 如果不熟悉 Direct3D,请阅读 DirectX 图形 SDK 文档中的“Direct3D 设备”和“Direct3D 资源”部分。
  • DirectShow 筛选器图或媒体基础管道,具体取决于应用程序将用于呈现视频的技术。
  • 媒体基础转换。 EVR 混合器是媒体基础转换,演示器直接在混合器上调用方法。
  • 实现 COM 对象。 演示器是进程内自由线程 COM 对象。

演示器对象模型

本部分包含演示器对象模型和接口的概述。

EVR 内部数据流

EVR 使用两个插件组件来呈现视频:混合器和演示器。 混合器混合视频流,并根据需要取消隔行扫描视频。 演示器将视频绘制(或呈现)到显示器上并计划何时绘制每个帧。 应用程序可以将其中任一对象替换为自定义实现。

EVR 有一个或多个输入流,混合器有相应数量的输入流。 流 0 始终是引用流。 其他流是子流,混合器将这些流阿尔法混合到引用流。 引用流确定复合视频的主帧速率。 对于每个参考帧,混合器从每个子流获取最新的帧,将其阿尔法混合到参考帧,并输出单个复合帧。 如果需要,混合器还会执行取消隔行扫描和从 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 的演示时钟正在运行。 演示器会计划视频帧在到达时进行演示。
  • Paused。 演示时钟已暂停。 演示器不演示任何新样本,但会保持其计划样本队列。 如果收到新样本,演示器会将它们添加到队列。
  • 已停止。 演示时钟已停止。 演示器放弃已计划的任何样本。
  • 关闭。 演示器释放与流式处理相关的任何资源,例如 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 使演示器能够处理受保护的媒体。 如果你的演示器是一个受信任的组件,用于在受保护的媒体路径 (PMP) 中工作,则实现此接口。
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 事件的模式,而不是媒体基础事件的模式。
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::ProcessOutput。 请参阅处理输出
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. 如果 llClockStartOffset 不是 PRESENTATION_CURRENT_POSITION,请刷新演示器的样本队列。 (这相当于接收 MFVP_MESSAGE_FLUSH 消息。
  3. 如果之前的单帧播放请求仍是待处理状态,请处理请求(请参阅单帧播放)。 否则,请尝试处理混合器的输出(请参阅处理输出
OnClockStop
  1. 将演示器状态设置为已停止。
  2. 刷新演示器的样本队列。
  3. 取消任何待处理的单帧播放操作。
OnClockPause 将演示器状态设置为已暂停。
OnClockRestart OnClockStart 的处理方式相同,但不要刷新样本队列。
OnClockSetRate
  1. 如果速率从零更改为非零,请取消单帧播放。
  2. 存储新的时钟速率。 时钟速率会影响演示样本的时间。 有关详细信息,请参阅计划样本

 

实现 IMFRateSupport

若要支持除 1× 速度以外的播放速率,演示器必须实现 IMFRateSupport 接口。 下面是实现此接口中方法的一些准则。 演示器关闭后,所有方法都应失败。 有关此接口的详细信息,请参阅速率控制

说明
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 方法时获取的。 (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 发生错误,需要停止流式处理。
  • Param1:指示错误发生的 HRESULT
  • 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. 创建作为原始类型的克隆的新媒体类型,然后更改以下属性:

    • MF_MT_FRAME_SIZE 属性设置为与要为 Direct3D 图面分配的宽度和高度相等。
    • MF_MT_PAN_SCAN_ENABLED 属性设置为 FALSE
    • MF_MT_PIXEL_ASPECT_RATIO 属性设置为与显示器的 PAR 相等(通常为 1:1)。
    • 将几何光圈(MF_MT_GEOMETRIC_APERTURE 属性)设置为与 Direct3D 图面中的矩形相等。 当混合器生成输出帧时,它会将源图像位块传送到此矩形上。 几何光圈可能像图面一样大,也可能是图面中的子矩形。 有关详细信息,请参阅源矩形和目标矩形
  4. 若要测试混合器是否接受修改后的输出类型,请调用具有 MFT_SET_TYPE_TEST_ONLY 标记的 IMFTransform::SetOutputType。 如果混合器拒绝该类型,请返回到步骤 1 并获取下一个类型。

  5. 分配 Direct3D 图面池,如分配 Direct3D 图面中所述。 在绘制复合视频帧时,混合器将使用这些图面。

  6. 通过调用没有标记的 SetOutputType 对混合器设置输出类型。 如果在步骤 4 中对 SetOutputType 的第一次调用成功,则该方法应该会再次成功

如果混合器类型用完,则 GetOutputAvailableType 方法返回 MF_E_NO_MORE_TYPES。 如果演示器找不到适合混合器的输出类型,则无法呈现流。 在这种情况下,DirectShow 或媒体基础可能会尝试其他流格式。 因此,演示器可能会在找到有效类型之前,一行中收到多个 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 指针。

创建设备后,如果演示器销毁设备并创建新的设备,则演示器必须再次调用 ResetDeviceResetDevice 方法使任何现有设备图柄失效,这会导致 LockDevice 返回 DXVA2_E_NEW_VIDEO_DEVICE。 该错误代码会向使用该设备的其他对象发出信号,提示它们应打开一个新的设备图柄。 有关使用设备管理器的详细信息,请参阅 Direct3D 设备管理器

演示器可以在窗口化模式或全屏独占模式下创建设备。 对于窗口化模式,应为应用程序提供指定视频窗口的方法。 为此,标准演示器实现 IMFVideoDisplayControl::SetVideoWindow 方法。 首次创建演示器时,必须创建设备。 通常,此时你并不知道所有设备参数,例如窗口或后台缓冲区格式。 可以创建临时设备,稍后将其替换&#;只需记住对设备管理器调用 ResetDevice

如果创建新设备,或者如果对现有设备调用 IDirect3DDevice9::ResetIDirect3DDevice9Ex::ResetEx,请将 EC_DISPLAY_CHANGED 事件发送到 EVR。 此事件通知 EVR 重新协商媒体类型。 EVR 忽略此事件的事件参数。

分配 Direct3D 图面

演示器设置媒体类型后,可以分配 Direct3D 图面,混合器将使用该图面来写入视频帧。 图面必须与演示器的媒体类型匹配:

  • 图面格式必须与媒体子类型匹配。 例如,如果子类型为 MFVideoFormat_RGB24,则图面格式必须为 D3DFMT_X8R8G8B8。 有关子类型和 Direct3D 格式的详细信息,请参阅视频子类型 GUID
  • 图面宽度和高度必须与媒体类型的 MF_MT_FRAME_SIZE 属性中提供的尺寸匹配。

分配图面的建议方式取决于演示器是窗口化运行还是全屏运行。

如果 Direct3D 设备是窗口化的,则可以创建若干交换链,每个交换链都有一个后台缓冲区。 使用此方法可以独立呈现每个图面,因为呈现一个交换链不会干扰其他交换链。 混合器可以将数据写入一个图面,同时计划另一个图面进行演示。

首先,确定要创建的交换链数。 建议至少三个。 对于每个交换链,请执行以下操作:

  1. 调用 IDirect3DDevice9::CreateAdditionalSwapChain 以创建交换链。
  2. 调用 IDirect3DSwapChain9::GetBackBuffer 以获取指向交换链的后台缓冲区图面的指针。
  3. 调用 MFCreateVideoSampleFromSurface 并传入指向图面的指针。 此函数返回指向视频样本对象的指针。 视频样本对象实现 IMFSample 接口,当演示器调用混合器的 IMFTransform::ProcessOutput 方法时,演示器使用此接口将图面传递给混合器。 有关视频样本对象的详细信息,请参阅视频样本
  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::ProcessOutput。 如果方法成功,演示器将计划演示样本。

若要从混合器获取输出,请执行以下步骤:

  1. 检查时钟状态。 如果时钟暂停,则忽略 MFVP_MESSAGE_PROCESSINPUTNOTIFY 消息,除非这是第一个视频帧。 如果时钟正在运行,或者这是第一个视频帧,请继续。

  2. 从可用样本队列中获取样本。 如果队列为空,则表示当前计划所有已分配的样本进行演示。 在这种情况下,请暂时忽略 MFVP_MESSAGE_PROCESSINPUTNOTIFY 消息。 当下一个样本可用时,重复此处列出的步骤。

  3. (可选。)如果时钟可用,通过调用 IMFClock::GetCorrelatedTime 获取当前时钟时间 (T1)。

  4. 对混合器调用 IMFTransform::ProcessOutput。 如果 ProcessOutput 成功,则样本包含视频帧。 如果方法失败,检查返回代码。 ProcessOutput 中的以下错误代码不是严重故障:

    错误代码 说明
    MF_E_TRANSFORM_NEED_MORE_INPUT 混合器需要更多输入,然后才能生成新的输出帧。
    如果收到此错误代码,检查 EVR 是否已到达流的末尾并相应地做出响应,如流结束中所述。 否则,请忽略此 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;
}

有关此示例的一些说明:

  • 假定 m_SamplePool 变量是保存可用视频样本队列的集合对象。 如果队列为空,则对象的 GetSample 方法返回 MF_E_SAMPLEALLOCATOR_EMPTY
  • 如果混合器的 ProcessOutput 方法返回 MF_E_TRANSFORM_NEED_MORE_INPUT,则表示混合器无法生成更多输出,因此演示器会清除 m_fSampleNotify 标志。
  • TrackSample 方法(该方法设置 IMFTrackedSample 回调)显示在跟踪样本部分。

重新绘制帧

有时,演示器可能需要重新绘制最新的视频帧。 例如,标准演示器在以下情况下会重新绘制帧:

使用以下步骤请求混合器重新创建最新的帧:

  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_ONEPresent 操作会等待显示器的垂直回描。 这是防止撕裂的一种简单方法,但会降低计划算法的准确性。 相反,如果演示间隔是 D3DPRESENT_INTERVAL_IMMEDIATEPresent 方法会立即执行,这会导致撕裂,除非计划算法准确到足以在垂直回描期间仅调用 Present

以下函数可帮助你获取准确的时间信息:

  • IDirect3DDevice9::GetRasterStatus 返回有关光栅的信息,包括当前扫描行以及光栅是否处于垂直空白期。
  • DwmGetCompositionTimingInfo 返回桌面窗口管理器的时间信息。 如果启用了桌面合成,则此信息非常有用。

演示样本

本部分假定你为每个图面创建了单独的交换链,如分配 Direct3D 图面中所述。 若要演示样本,请从视频样本获取交换链,如下所示:

  1. 对视频样本调用 IMFSample::GetBufferByIndex 以获取缓冲区。
  2. 查询 IMFGetService 接口的缓冲区。
  3. 调用 IMFGetService::GetService 以获取 Direct3D 图面的 IDirect3DSurface9 接口。 (可以通过调用 MFGetService 将此步骤和上一步合并为一个步骤。)
  4. 对图面调用 IDirect3DSurface9::GetContainer 以获取指向交换链的指针。
  5. 对交换链调用 IDirect3DSwapChain9::Present

以下代码演示了这些步骤:

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 属性设置源矩形。 当混合器将视频位块传送到目标图面时,混合器将应用源矩形。 混合器的默认源矩形是整个帧。
  • 将目标矩形设置为混合器输出类型中的几何光圈。 有关详细信息,请参阅协商格式

第二种方式是当 IDirect3DSwapChain9::Present 时应用矩形,方法是在 Present 方法中指定 pSourceRectpDestRect。 可以组合使用这些方式。 例如,可以在混合器上设置源矩形,但在 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::ProcessMessage 方法中,将 m_fEndStreaming 设置为 TRUE,并在 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::ProcessOutput 方法返回 MF_E_TRANSFORM_NEED_MORE_INPUT,调用 CheckEndOfStream。 此错误代码指示混合器没有更多输入样本(请参阅处理输出)。

单帧播放

EVR 可用于支持 DirectShow 中的单帧播放和媒体基础中的选段播放。 单帧播放和选段播放在概念上相似。 在这两种情况下,应用程序一次请求一个视频帧。 在内部,演示器使用相同的机制来实现这两个功能。

DirectShow 中单帧播放的原理如下所示:

  • 应用程序调用 IVideoFrameStep::StepdwSteps 参数中提供步数。 EVR 将 MFVP_MESSAGE_STEP 消息发送到演示器,其中消息参数 (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 在单帧播放操作仍处于待处理状态时取消单帧播放,则演示器会发送 EC_STEP_COMPLETE 事件,且 lParam1 设置为 TRUE

应用程序可以单帧播放或选段播放多次,因此演示器可能会在收到 MFVP_MESSAGE_CANCELSTEP 消息前,收到多个 MFVP_MESSAGE_STEP 消息。 此外,演示器可以在时钟开始前或时钟正在运行时接收 MFVP_MESSAGE_STEP 消息。

实现单帧播放

本部分介绍一种实现单帧播放的算法。 单帧播放算法使用以下变量:

  • step_count。 一个无符号整数,指定当前单帧播放操作中的步数。

  • step_queueIMFSample 指针的队列。

  • step_state。 在任何时间,演示器都可能处于以下单帧播放状态之一:

    州/省/市/自治区 说明
    NOT_STEPPING 没有单帧播放。
    WAITING 演示器已收到 MFVP_MESSAGE_STEP 消息,但时钟尚未启动。
    PENDING 演示器已收到 MFVP_MESSAGE_STEP 消息且时钟已启动,但演示器正在等待接收目标帧。
    SCHEDULED 演示器已收到目标帧,并已计划它进行演示,但尚未呈现该帧。
    完成 演示器已呈现目标帧并发送 EC_STEP_COMPLETE 事件,正在等待下一个 MFVP_MESSAGE_STEPMFVP_MESSAGE_CANCELSTEP 消息。

     

    这些状态独立于演示器状态部分中列出的演示器状态。

为单帧播放算法定义了以下过程:

PrepareFrameStep 过程

  1. 递增 step_count
  2. step_state 设置为 WAITING。
  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. 发送 EC_STEP_COMPLETE 事件,且 lParam1 = FALSE
  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,发送 EC_STEP_COMPLETE,且 lParam1 = TRUE

按如下所示调用这些过程:

演示器消息或方法 过程
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 激活对象。 有关激活对象的详细信息,请参阅激活对象

对于 EVR 媒体接收器,请执行以下操作:

  1. 调用 MFCreateVideoRenderer 以创建媒体接收器。
  2. 创建演示器的实例。
  3. 查询 IMFVideoRenderer 接口的 EVR 媒体接收器。
  4. 调用 IMFVideoRenderer::InitializeRenderer

对于 EVR 激活对象,请执行以下操作:

  1. 调用 MFCreateVideoRendererActivate 以创建激活对象。

  2. 在激活对象上设置以下属性之一:

    Attribute 说明
    MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_ACTIVATE 指向演示器的激活对象的指针。
    对于此标志,必须为演示器提供激活对象。 激活对象必须实现 IMFActivate 接口。
    MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_CLSID 演示器的 CLSID。
    对于此标志,演示器必须支持通过 IClassFactory 创建标准 COM 对象。

     

  3. (可选)对激活对象设置 MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_FLAGS 属性。

增强的视频呈现器

EVRPresenter 样本