Async Filter Sample

I've been looking at the "Async" filter sample for DirectShow, which shows how to implement IAsyncReader. The sample is an odd combination of useful and not-so-useful code. (Or maybe that's not so odd in an SDK sample.)

Alessandro Angeli, one of the DirectShow MVPs, has a good post about using this sample:

https://groups.google.com/group/microsoft.public.win32.programmer.directx.video/msg/01dde303fa3088c5

[The only comment I'd make is that CAsyncStream is not actually the output pin.]

Here are some notes about this sample.

The source files are located in 4 folders:

  • "Base" has base classes for the filter.
  • "Filter" has the filter itself, plus the DLL setup stuff.
  • "Include" has include files for both of the above.
  • "MemFile" is a command-line application that uses the filter.

Here's how the code is organized. First the base classes:

  • CAsyncReader is the base filter class
  • CAsyncOutputPin is the output pin. It is instantiated in CAsyncReader::m_OutputPin.
  • CAsyncStream is an abstract class that models I/O operations. The CAsyncReader constructor takes a pointer to this class. The derived class is expected to implement I/O operations using the interface defined by this class.
  • The filter queues requests in the form of CAsyncResult objects.
  • CAsyncIo runs the thread that dispatches the requests. It is instantiated in CAsyncReader::m_Io, and the output pin has a pointer to it in CAsyncOutputPin::m_pIo.

And the filter implementation:

  • CAsyncFilter is the example filter. It derives from CAsyncReader
  • CMemStream derives from CAsyncStream and does "in memory" I/O from a memory buffer.

When the downstream filter (ie the parser) calls IAsyncRequest::Request, the pin calls CAsyncIO::Request, which:

  1. Creates a new CAsyncRequest object.
  2. Calls CAsyncRequest::Request(), which makes the I/O request. That's in theory, but see below.
  3. Puts the object on the queue.

The worker thread pulls CAsyncRequest objects from the queue and calls CAsyncRequest::Complete, which completes the I/O request.

However, as implemented, CAsyncRequest::Request does not actually make an I/O request. Instead, it just caches the parameters. The Complete() method does all the work, by calling CAsyncStream::Read. So in fact, the filter performs synchronous I/O, but on a worker thread.

When the pin is flushed, it calls CAsyncRequest::Cancel() on all pending requests. However, this method does not actually do anything; it simply returns S_OK.

The CAsyncFilter class is sort of peculiar. It reads the entire file into memory when IFileSourceFilter::Load

is called. Then, CMemStream reads the data that's already in memory. Curiously, CMemStream actually has a bits/second parameter, which can be used to artificially throttle read operations (by calling Sleep!).

If I were redesigning this sample, I would change the definition of CAsyncStream to support truly async I/O calls. Something along the lines of:

 virtual HRESULT StartRead(
    PBYTE pbBuffer,
    DWORD dwBytesToRead,
    BOOL bAlign,
    LPOVERLAPPED pOverlapped, /* [in] Caller-supplied OVERLAPPED structure */
    LPBOOL pbPending, /* [out] If TRUE, call EndRead to wait for completion */
    LPDWORD pdwBytesRead /* [out] Bytes read, if pbPending receives FALSE */
    ) = 0;

virtual HRESULT EndRead(
    LPOVERLAPPED pOverlapped, 
    LPDWORD pdwBytesRead
    ) = 0;

Incidentally, IAsyncReader is not a very good interface for network streaming, because it assumes you can seek arbitrarily within the stream. For network streaming, it is better to use a push model. If you control both the network protocol and the stream format, you can combine these into a single source filter that does both the network IO and the stream parsing.