Adding Seeking to the Ball Filter

Microsoft Windows Digital Media Division

December 2001

Summary: This article describes how to implement seeking in a Microsoft DirectShow source filter. It uses the Ball Filter sample from the DirectShow Software Development Kit (SDK) as the starting point and describes the additional code needed to support seeking in this filter. (10 printed)

Download SeekingSample.exe.

Introduction

The Ball Filter sample in the Microsoft® DirectShow® Software Development Kit (SDK) is a source filter that creates an animated bouncing ball. This article describes how to add seeking functionality to this filter. Once you have added this functionality, you can render the filter in GraphEdit and control the ball by dragging the GraphEdit slider. You can even use the seekable Ball filter as a source clip in DirectShow Editing Services and alpha blend it with another clip.

This article assumes that you are familiar with DirectShow programming and have a general understanding of the DirectShow filter graph architecture.

This article covers the following topics:

  • Overview of Seeking in DirectShow
    Gives a brief overview of how seeking is handled by the DirectShow architecture.
  • Quick Overview of the Ball Filter
    Describes the general design of the Ball Filter sample.
  • Modifying the Ball Filter for Seeking
    Describes the additional code needed to add seeking functionality to the Ball filter.
  • Using the Ball in a DirectShow Editing Services Project
    Shows how to use the modified Ball filter in a DirectShow Editing Services project.
  • Limitations of the CSourceSeeking Class
    Provides some tips for implementing seeking when the CSourceSeeking class is insufficient.

Overview of Seeking in DirectShow

An application seeks the filter graph by calling an IMediaSeeking method on the Filter Graph Manager (or by calling an IMediaPosition method, but the Filter Graph Manager translates these into IMediaSeeking calls). The Filter Graph Manager then distributes the call to every renderer in the graph. Each renderer sends the call upstream, through the output pin of the next upstream filter. The call travels upstream until it reaches a filter that can execute the seek command, typically a source filter or a parser filter. In general, the filter that originates the time stamps also handles seeking.

A filter responds to a seek command as follows:

  1. The filter flushes the graph. This clears any stale data from graph, which improves responsiveness. Otherwise, samples that were buffered prior to the seek command might get delivered.
  2. The filter calls IPin::NewSegment to inform downstream filters of the new stop time, start time, and playback rate.
  3. The filter then sets the discontinuity flag on the first sample after the seek command.

Time stamps start from zero after any seek command (including rate changes).

Quick Overview of the Ball Filter

The Ball filter is a push source, which means that it uses a worker thread to deliver samples downstream, as opposed to a pull source, which passively waits for a downstream filter to request samples. The Ball filter is built from the CSource class, and its output pin is built from the CSourceStream class. The CSourceStream class creates the worker thread that drives the flow of data. This thread enters a loop that gets samples from the allocator, fills them with data, and delivers them downstream.

Most of the action in CSourceStream happens in the FillBuffer method, which the derived class implements. The argument to this method is a pointer to the sample to be delivered. The Ball filter's implementation of FillBuffer retrieves the address of the sample buffer and draws directly to the buffer by setting individual pixel values. (The drawing is done by a helper class, CBall, so you can ignore the details regarding bit depths, palettes, and so forth.)

Modifying the Ball Filter for Seeking

To make the Ball filter seekable, use the CSourceSeeking class, which is designed for implementing seeking in filters with one output pin. Add the CSourceSeeking class to the inheritance list for the CBallStream class:

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

Also, you will need to add an initializer for CSourceSeeking to the CBallStream constructor:

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

This statement calls the base constructor for CSourceSeeking. The parameters are a name, a pointer to the owning pin, an HRESULT value, and the address of a critical section object. The name is used only for debugging, and the NAME macro compiles to an empty string in retail builds. The pin directly inherits CSourceSeeking, so the second parameter is a pointer to itself, cast to an IPin pointer. The HRESULT value is ignored in the current version of the base classes; it remains for compatibility with previous versions. The critical section protects shared data, such as the current start time, stop time, and playback rate.

Add the following statement inside the CSourceSeeking constructor:

m_rtStop = 60 * UNITS;

The m_rtStop variable specifies the stop time. By default, the value is _I64_MAX / 2, which is approximately 14,600 years. The previous statement sets it to a more conservative 60 seconds.

Two additional member variables must be added to CBallStream:

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

After every seek command, the filter must set the discontinuity flag on the next sample by calling IMediaSample::SetDiscontinuity. The m_bDiscontinuity variable will keep track of this. The m_rtBallPosition variable will specify the position of the ball within the video frame. The original Ball filter calculates the position from the stream time, but the stream time resets to zero after each seek command. In a seekable stream, the absolute position is independent of the stream time.

QueryInterface

The CSourceSeeking class implements the IMediaSeeking interface. To expose this interface to clients, override the NonDelegatingQueryInterface method:

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

The method is called "NonDelegating" because of the way the DirectShow base classes support Component Object Model (COM) aggregation. For more information, see the "How to Implement IUnknown" topic in the DirectShow SDK.

Seeking Methods

The CSourceSeeking class maintains several member variables relating to seeking.

Variable Description Default Value
m_rtStart Start time Zero
m_rtStop Stop time _I64_MAX / 2
m_dRateSeeking Playback rate 1.0

The CSourceSeeking implementation of IMediaSeeking::SetPositions updates the start and stop times, and then calls two pure virtual methods on the derived class, ChangeStart and ChangeStop. The implementation of SetRate is similar: It updates the playback rate and then calls the pure virtual method ChangeRate. In each of these virtual methods, the pin must do the following:

  1. Call IPin::BeginFlush to start flushing data.
  2. Halt the streaming thread.
  3. Call IPin::EndFlush.
  4. Restart the streaming thread.
  5. Call IPin::NewSegment.
  6. Set the discontinuity flag on the next sample.

The order of these steps is crucial, because the streaming thread can block while it waits to deliver a sample or get a new sample. The BeginFlush method ensures that the streaming thread is not blocked and therefore will not deadlock in step 2. The EndFlush call informs the downstream filters to expect new samples, so they do not reject them when the thread starts again in step 4.

The following private method performs steps 1 through 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();
    }
}

When the streaming thread starts again, it calls the CSourceStream::OnThreadStartPlay method. Override this method to perform steps 5 and 6:

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

In the ChangeStart method, set the stream time to zero and the position of the ball to the new start time. Then call UpdateFromSeek:

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

In the ChangeStop method, call UpdateFromSeek if the new stop time is less than the current position. Otherwise, the stop time is still in the future, so there's no need to flush the graph.

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;
}

For rate changes, the CSourceSeeking::SetRate method sets m_dRateSeeking to the new rate (discarding the old value) before it calls ChangeRate. Unfortunately, if the caller gave an invalid rate—for example, less than zero—it's too late by the time ChangeRate is called. One solution is to override SetRate and check for valid rates:

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; }
Drawing in the Buffer

Here is the modified version of FillBuffer, the routine that draws the ball on each frame:

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;
} 

The major differences between this version and the original are the following:

  • As mentioned earlier, the m_rtBallPosition variable is used to set the position of the ball, rather than the stream time.
  • After holding the critical section, the method checks whether the current position exceeds the stop time. If so, it returns S_FALSE, which signals the base class to stop sending data and deliver an end-of-stream notification.
  • The time stamps are divided by the current rate.
  • If m_bDiscontinuity is True, the method sets the discontinuity flag on the sample.

There is another minor difference. Because the original version relies on having exactly one buffer, it fills the entire buffer with zeroes once, when streaming begins. After that, it just erases the ball from its previous position. However, this optimization reveals a slight bug in the Ball filter. When the CBaseOutputPin::DecideAllocator method calls IMemInputPin::NotifyAllocator, it sets the read-only flag to FALSE. As a result, the downstream filter is free to write on the buffer. That's not a problem if you connect the Ball filter to the Video Renderer, because the Video Renderer will not write on the sample. However, DirectShow Editing Services does write on the samples it receives, which breaks this optimization. One solution is to override DecideAllocator so that it sets the read-only flag to TRUE. For simplicity, however, the new version simply removes the optimization altogether. Instead, this version fills the buffer with zeroes each time.

Miscellaneous Changes

In the new version, these two lines are removed from the CBall constructor:

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

The original Ball filter uses these values to add some randomness to the initial ball position. For our purposes, we want the ball to be deterministic. Also, some variables have been changed from CRefTime objects to REFERENCE_TIME variables. (The CRefTime class is a thin wrapper for a REFERENCE_TIME value.) Lastly, the implementation of IQualityControl::Notify has been modified slightly you can refer to the source code for details.

Using the Ball Filter in a DirectShow Editing Services Project

Here's a little trick you can do with the Ball filter, once you've added seeking functionality. Paste the following into a text file named ball.xtl:

<timeline>
  <group type="video" framerate="15.0" bitdepth="24" 
       width="320" height="240">
    <track>
      <clip src="C:\Example.avi" start="0" stop="5" />
    </track>
    <track>
      <clip clsid="{fd501041-8ebe-11ce-8183-00aa00577da1}" 
         start="0" stop="5" />
      <transition clsid="{C5B19592-145E-11D3-9F04-006008039E37}" 
         start="0" stop="5">
        <param name="RGB" value="0" />
        <param name="KeyType" value="0" />
      </transition>
    </track>
  </group>
</timeline>

Change the string "Example.avi" to the name of a video file on your system. Now compile the XtlTest sample that comes with the Microsoft DirectX® 8.1 SDK, and run it from the command line:

xtltest ball.xtl

You will see the ball bounce in front of the video clip. This XTL file creates a DirectShow Editing Services project with two video tracks. One track contains the video from the source file, and the other uses the Ball filter to generate the video. The XTL file also defines a chroma key transition, which composites the two video images. The chroma key uses the Ball filter's black background as the key color. This works because DirectShow Editing Services can use a source filter as a source clip, if the filter supports seeking.

Limitations of the CSourceSeeking Class

The CSourceSeeking class is not meant for filters with multiple output pins, because of issues with cross-pin communication. For example, imagine a parser filter that receives an interleaved audio-video stream, splits the stream into its audio and video components, and delivers video from one output pin and audio from another. Both output pins will receive every seek command, but the filter should seek only once per seek command. The solution is to designate one of the pins to control seeking and to ignore seek commands received by the other pin.

After the seek command, however, both pins should flush data. To complicate matters further, the seek command happens on the application thread, not the streaming thread. Therefore, you must make certain that neither pin is blocked and waiting for a Receive call to return, or it might cause a deadlock. For more information about thread-safe flushing in pins, see the "Threads and Critical Sections" topic in the DirectShow SDK documentation.

For More Information

To learn more about DirectShow, see the DirectShow SDK documentation on MSDN.