支持在源筛选器中查找

[与此页面关联的功能 DirectShow 是一项旧功能。 它已被 MediaPlayerIMFMediaEngine媒体基金会中的音频/视频捕获取代。 这些功能已针对Windows 10和Windows 11进行了优化。 Microsoft 强烈建议新代码尽可能使用 MediaPlayerIMFMediaEngineMedia Foundation 中的音频/视频捕获 ,而不是 DirectShow。 如果可能,Microsoft 建议重写使用旧 API 的现有代码以使用新 API。]

本主题介绍如何在 Microsoft DirectShow 源筛选器中实现查找。 它使用 Ball Filter 示例作为起点,并描述支持在此筛选器中查找所需的其他代码。

球筛选器示例是创建动画弹跳球的源筛选器。 本文介绍如何向此筛选器添加查找功能。 添加此功能后,可以在 GraphEdit 中呈现筛选器,并通过拖动 GraphEdit 滑块来控制球。

本主题包含以下各节:

在 DirectShow 中查找概述

应用程序通过在筛选器关系图管理器上调用 IMediaSeeking 方法来查找筛选器图。 然后,筛选器关系图管理器将调用分发到图形中的每个呈现器。 每个呈现器通过下一个上游筛选器的输出引脚发送调用上游。 调用上游,直到到达可以执行 seek 命令的筛选器,通常是源筛选器或分析器筛选器。 通常,源自时间戳的筛选器也会处理查找。

筛选器响应 seek 命令,如下所示:

  1. 筛选器刷新图形。 这将清除图形中的任何过时数据,从而提高响应能力。 否则,可能会传递在 seek 命令之前缓冲的样本。
  2. 筛选器调用 IPin::NewSegment ,以通知下游筛选器新的停止时间、开始时间和播放速率。
  3. 然后,筛选器在 seek 命令后对第一个样本设置不连续标志。

在任何 seek 命令 (包括速率更改) 之后,时间戳从零开始。

球过滤器快速概述

Ball 筛选器是一个推送源,这意味着它使用工作线程在下游传递样本,而不是被动地等待下游筛选器请求样本的拉取源。 Ball 筛选器是从 CSource 类生成的,其输出引脚是从 CSourceStream 类生成的。 CSourceStream 类创建驱动数据流的工作线程。 此线程进入一个循环,该循环从分配器获取样本,用数据填充这些样本,并将它们传送到下游。

CSourceStream 中的大多数操作发生在派生类实现的 CSourceStream::FillBuffer 方法中。 此方法的参数是指向要传递的示例的指针。 Ball 筛选器的 FillBuffer 实现检索示例缓冲区的地址,并通过设置单个像素值直接绘制到缓冲区。 (绘图由帮助程序类 CBall 完成,因此你可以忽略有关位深度、调色板等的详细信息。)

修改用于查找的球筛选器

若要使 Ball 筛选器可查找,请使用 CSourceSeeking 类,该类旨在通过一个输出引脚实现在筛选器中查找。 将 CSourceSeeking 类添加到 CBallStream 类的继承列表:

class CBallStream :  // Defines the output pin.
    public CSourceStream, public CSourceSeeking

此外,还需要将 CSourceSeeking 的初始值设定项添加到 CBallStream 构造函数:

    CSourceSeeking(NAME("SeekBall"), (IPin*)this, phr, &m_cSharedState),

此语句调用 CSourceSeeking 的基本构造函数。 参数是名称、指向所属引脚的指针、 HRESULT 值和关键节对象的地址。 该名称仅用于调试, NAME 宏在零售版本中编译为空字符串。 引脚直接继承 CSourceSeeking,因此第二个参数是指向自身的指针,强制转换为 IPin 指针。 在当前版本的基类中忽略 HRESULT 值;保留它是为了与以前的版本兼容。 关键部分保护共享数据,例如当前开始时间、停止时间和播放速率。

CSourceSeeking 构造函数中添加以下语句:

m_rtStop = 60 * UNITS;

m_rtStop变量指定停止时间。 默认情况下,该值为 _I64_MAX/2,大约为 14,600 年。 上一语句将其设置为更保守的 60 秒。

必须向 CBallStream 添加两个附加成员变量:

BOOL            m_bDiscontinuity; // If true, set the discontinuity flag.
REFERENCE_TIME  m_rtBallPosition; // Position of the ball. 

在每次查找命令后,筛选器必须通过调用 IMediaSample::SetDiscontinuity 来设置下一个示例的不连续标志。 m_bDiscontinuity变量将对此进行跟踪。 m_rtBallPosition变量将指定球在视频帧中的位置。 原始 Ball 筛选器根据流时间计算位置,但在每次查找命令后,流时间重置为零。 在可查找流中,绝对位置与流时间无关。

QueryInterface

CSourceSeeking 类实现 IMediaSeeking 接口。 若要向客户端公开此接口,请重写 NonDelegatingQueryInterface 方法:

STDMETHODIMP CBallStream::NonDelegatingQueryInterface
    (REFIID riid, void **ppv)
{
    if( riid == IID_IMediaSeeking ) 
    {
        return CSourceSeeking::NonDelegatingQueryInterface( riid, ppv );
    }
    return CSourceStream::NonDelegatingQueryInterface(riid, ppv);
}

由于 DirectShow 基类支持组件对象模型 (COM) 聚合的方式,因此该方法称为“NonDelegating”。 有关详细信息,请参阅 DirectShow SDK 中的“如何实现 IUnknown”主题。

查找方法

CSourceSeeking 类维护与查找相关的多个成员变量。

变量 说明 默认值
m_rtStart 开始时间 零个
m_rtStop 停止时间 _I64_MAX / 2
m_dRateSeeking 播放速率 1.0

 

IMediaSeeking::SetPositionsCSourceSeeking 实现更新开始和停止时间,然后在派生类上调用两个纯虚拟方法:CSourceSeeking::ChangeStartCSourceSeeking::ChangeStopIMediaSeeking::SetRate 的实现类似:它更新播放速率,然后调用纯虚拟方法 CSourceSeeking::ChangeRate。 在每个虚拟方法中,引脚必须执行以下操作:

  1. 调用 IPin::BeginFlush 开始刷新数据。
  2. 停止流式处理线程。
  3. 调用 IPin::EndFlush
  4. 重启流式处理线程。
  5. 调用 IPin::NewSegment
  6. 在下一个示例上设置不连续标志。

这些步骤的顺序至关重要,因为流式处理线程在等待提供示例或获取新示例时可能会阻塞。 BeginFlush 方法可确保流式处理线程不会被阻止,因此不会在步骤 2 中死锁。 EndFlush 调用通知下游筛选器预期新样本,因此当线程在步骤 4 中再次启动时,它们不会拒绝这些样本。

以下私有方法执行步骤 1 到 4:

void CBallStream::UpdateFromSeek()
{
    if (ThreadExists()) 
    {
        DeliverBeginFlush();
        // Shut down the thread and stop pushing data.
        Stop();
        DeliverEndFlush();
        // Restart the thread and start pushing data again.
        Pause();
    }
}

当流式处理线程再次启动时,它将调用 CSourceStream::OnThreadStartPlay 方法。 重写此方法以执行步骤 5 和 6:

HRESULT CBallStream::OnThreadStartPlay()
{
    m_bDiscontinuity = TRUE;
    return DeliverNewSegment(m_rtStart, m_rtStop, m_dRateSeeking);
}

ChangeStart 方法中,将流时间设置为零,将球的位置设置为新的开始时间。 然后调用 CBallStream::UpdateFromSeek:

HRESULT CBallStream::ChangeStart( )
{
    {
        CAutoLock lock(CSourceSeeking::m_pLock);
        m_rtSampleTime = 0;
        m_rtBallPosition = m_rtStart;
    }
    UpdateFromSeek();
    return S_OK;
}

ChangeStop 方法中,如果新的停止时间小于当前位置,则调用 CBallStream::UpdateFromSeek。 否则,停止时间仍在将来,因此无需刷新图形。

HRESULT CBallStream::ChangeStop( )
{
    {
        CAutoLock lock(CSourceSeeking::m_pLock);
        if (m_rtBallPosition < m_rtStop)
        {
            return S_OK;
        }
    }

    // We're already past the new stop time. Flush the graph.
    UpdateFromSeek();
    return S_OK;
}

对于速率更改, CSourceSeeking::SetRate 方法 将m_dRateSeeking 设置为新速率, (在调用 ChangeRate 之前放弃旧值) 。 遗憾的是,如果调用方给出的速率无效(例如,小于零),那么在调用 ChangeRate 时为时已晚。 一种解决方案是替代 SetRate,并针对有效速率检查:

HRESULT CBallStream::SetRate(double dRate)
{
    if (dRate <= 1.0)
    {
        return E_INVALIDARG;
    }
    {
        CAutoLock lock(CSourceSeeking::m_pLock);
        m_dRateSeeking = dRate;
    }
    UpdateFromSeek();
    return S_OK;
}
// Now ChangeRate won't ever be called, but it's pure virtual, so it needs
// a dummy implementation.
HRESULT CBallStream::ChangeRate() { return S_OK; }

在缓冲区中绘图

下面是 CSourceStream::FillBuffer 的修改版本,该例程在每个帧上绘制球:

HRESULT CBallStream::FillBuffer(IMediaSample *pMediaSample)
{
    BYTE *pData;
    long lDataLen;
    pMediaSample->GetPointer(&pData);
    lDataLen = pMediaSample->GetSize();
    {
        CAutoLock cAutoLockShared(&m_cSharedState);
        if (m_rtBallPosition >= m_rtStop) 
        {
            // End of the stream.
            return S_FALSE;
        }
        // Draw the ball in its current position.
        ZeroMemory( pData, lDataLen );
        m_Ball->MoveBall(m_rtBallPosition);
        m_Ball->PlotBall(pData, m_BallPixel, m_iPixelSize);
        
        // The sample times are modified by the current rate.
        REFERENCE_TIME rtStart, rtStop;
        rtStart = static_cast<REFERENCE_TIME>(
                      m_rtSampleTime / m_dRateSeeking);
        rtStop  = rtStart + static_cast<int>(
                      m_iRepeatTime / m_dRateSeeking);
        pMediaSample->SetTime(&rtStart, &rtStop);

        // Increment for the next loop.
        m_rtSampleTime += m_iRepeatTime;
        m_rtBallPosition += m_iRepeatTime;
    }
    pMediaSample->SetSyncPoint(TRUE);
    if (m_bDiscontinuity) 
    {
        pMediaSample->SetDiscontinuity(TRUE);
        m_bDiscontinuity = FALSE;
    }
    return NOERROR;
}

此版本与原始版本之间的主要区别如下:

  • 如前所述, m_rtBallPosition 变量用于设置球的位置,而不是流时间。
  • 保留关键部分后,方法检查当前位置是否超过停止时间。 如果是这样,它将返回 S_FALSE,这指示基类停止发送数据并发送流结束通知。
  • 时间戳除以当前速率。
  • 如果m_bDiscontinuity为 TRUE,则 该方法在示例上设置不连续标志。

还有一个细微的区别。 由于原始版本依赖于恰好有一个缓冲区,因此当流式处理开始时,它会用零填充整个缓冲区一次。 之后,它只是将球从先前的位置上抹去。 但是,此优化在 Ball 筛选器中显示一个轻微的 bug。 当 CBaseOutputPin::D ecideAllocator 方法调用 IMemInputPin::NotifyAllocator 时,它将只读标志设置为 FALSE。 因此,下游筛选器可以自由地在缓冲区上写入。 一种解决方案是重写 DecideAllocator ,以便将只读标志设置为 TRUE。 但是,为简单起见,新版本只是完全删除优化。 相反,此版本每次都会用零填充缓冲区。

其他更改

在新版本中,这两行将从 CBall 构造函数中删除:

    m_iRandX = rand();
    m_iRandY = rand();

原始的 Ball 筛选器使用这些值向初始球位置添加一些随机性。 出于我们的目的,我们希望球是确定性的。 此外,某些变量已从 CRefTime 对象更改为 REFERENCE_TIME 变量。 (CRefTime 类是 REFERENCE_TIME 值的精简包装器。) 最后, 对 IQualityControl::Notify 的实现进行了轻微修改;有关详细信息,请参阅源代码。

CSourceSeeking 类的限制

CSourceSeeking 类不适用于具有多个输出引脚的筛选器,因为跨引脚通信存在问题。 例如,假设一个分析器筛选器接收交错的音频视频流,将流拆分为其音频和视频组件,并从一个输出引脚传送视频,从另一个输出引脚传送音频。 这两个输出引脚将接收每个搜寻命令,但筛选器应为每个搜寻命令只查找一次。 解决方法是指定其中一个引脚来控制查找,并忽略另一个引脚收到的查找命令。

但是,在 seek 命令之后,两个引脚都应刷新数据。 使问题进一步复杂化,seek 命令发生在应用程序线程上,而不是流式处理线程上。 因此,必须确保这两个引脚都不会被阻止,也不会等待 IMemInputPin::Receive 调用返回,否则可能会导致死锁。 有关引脚中线程安全刷新的详细信息,请参阅 线程和关键部分

编写源筛选器