DirectShow Pipeline
This document explains how WinDV uses DirectShow, why custom filter wrappers are needed, and the exact filter graph topology for every operating mode.
Related reading: Architecture for the class hierarchy, Threading Model for how frames are routed between threads.
What DirectShow Is and Why WinDV Uses It
DirectShow is Microsoft’s COM-based multimedia framework included in Windows since Windows 98. It provides:
- A filter graph: a directed graph of COM objects (filters) connected by pins, each transforming or transporting a media stream.
- System filters for common tasks: AVI file source/sink, AVI Splitter, AVI Mux, DV decoder, DV Splitter, DV Mux.
- Device enumeration and capture graph construction helpers
(
ICreateDevEnum,ICaptureGraphBuilder2). - Transport control interfaces such as
IMediaControl(run/pause/stop) andIAMExtTransport(tape deck control).
WinDV uses DirectShow because:
- The Windows FireWire DV driver exposes itself as a DirectShow capture filter. There is no other standard API to read raw DV frames from IEEE 1394.
- AVI file writing and reading via AVI Mux and AVI Splitter are provided by DirectShow’s system filters, avoiding the need to implement AVI container logic.
- The DV decoder (hardware-accelerated on many systems) and video renderer are standard DirectShow filters, making preview trivial.
CFilterGraph – Base Wrapper
CFilterGraph (declared in DShow.h, implemented in DShow.cpp) is the
base class for every filter graph variant in WinDV.
Its constructor creates and wires five COM interfaces, all owned by the
same underlying filter graph COM object:
| Member | Interface | Purpose |
|---|---|---|
m_GB |
ICaptureGraphBuilder2 |
Helper for connecting capture topologies |
m_FG |
IGraphBuilder |
Filter graph manager; owns all filters |
m_MC |
IMediaControl |
Run / Pause / Stop the graph |
m_MS |
IMediaSeeking |
Duration and position queries |
m_ME |
IMediaEventEx |
Completion events (EC_COMPLETE) |
m_GB is initialised with m_GB->SetFiltergraph(m_FG) so that
ICaptureGraphBuilder2::RenderStream() operates on the same graph as
direct IGraphBuilder calls.
The destructor calls m_MC->Stop() before releasing interfaces to ensure
no frames are delivered after the subclass’s pipeline objects are gone.
Bridging the Push Model: CInputGraph and COutputGraph
DirectShow is a push model: the source filter produces data and pushes it downstream through pin connections. WinDV needs to intercept that data and route it through its own ring buffer and worker threads, which operate outside the DirectShow graph.
Two custom filter wrappers solve this:
CInputGraph – Custom Sink Filter
CInputGraph builds a filter graph that terminates in a custom sink
filter (CInputFilter) containing one input pin (CInputPin).
[upstream source filters]
|
v
CInputFilter / CInputPin (CBaseInputPin)
| CInputPin::Receive()
v
CInputGraph::m_handler->HandleFrame()
CInputPin::Receive() is the hot path, called once per frame at 25 or
30 Hz on DirectShow’s streaming thread.
It extracts the frame duration and raw bytes from IMediaSample and calls
the registered CFrameHandler.
CInputFilter uses CLSID_NULL as its CLSID because it is never
registered in the system registry; it exists only within the process’s
graph.
CheckMediaType() on CInputPin accepts any stream typed as
MEDIATYPE_Interleaved, which covers both raw DV from FireWire and the
interleaved DV stream produced by a DV Mux when reading Type-2 AVI files.
COutputGraph – Custom Source Filter
COutputGraph builds a filter graph that originates from a custom
source filter (COutputFilter) containing one output pin (COutputPin).
COutputGraph::HandleFrame()
| acquires IMediaSample, copies data, sets timestamps
v
COutputFilter / COutputPin (CBaseOutputPin)
|
v
[downstream sink filters]
HandleFrame() packages raw DV bytes into an IMediaSample, assigns a
monotonically increasing presentation timestamp range
[m_time, m_time + duration], marks it as a sync point (all DV frames are
independently decodable), and calls Deliver().
When m_queue > 0, COutputPin::Active() creates a COutputQueue – a
DirectShow helper that moves sample delivery onto a dedicated thread
running at THREAD_PRIORITY_ABOVE_NORMAL.
This prevents HandleFrame() on the recording thread from blocking if
the downstream filter (AVI Mux or DV device) is momentarily slow.
CDVOutput requests a queue depth of 10 to absorb FireWire timing jitter.
CAVIWriter uses synchronous delivery (queue depth 0).
Capture Pipeline: CDVInput to CAVIWriter
CDVInput
CDVInput is a CInputGraph + CDVControl subclass.
Graph topology:
DV capture filter (FireWire, friendly name = vsrc)
| MEDIATYPE_Interleaved
v
CInputFilter (registered as "InputFilter" in m_FG)
Construction steps (CDVInput::CDVInput):
-
EnumVideoDevices()enumeratesCLSID_VideoInputDeviceCategoryviaICreateDevEnum/IEnumMonikerand binds the named device toIBaseFilter *pVSRC. -
CtrlAttach(pVSRC)queriesIAMExtTransportfrom the device filter so that tape transport commands (play/pause/stop) work if supported. -
m_FG->AddFilter(pVSRC, L"DVin")registers the capture filter. -
m_GB->RenderStream(NULL, &MEDIATYPE_Interleaved, pVSRC, NULL, m_inputFilter)connects the capture pin toCInputFilter. -
IAMDroppedFramesis queried from the capture pin’s upstream output pin so dropped frame counts can be reported to the UI.
CDV::HandleFrame and CDVQueue
CDV implements CFrameHandler.
CDV::HandleFrame() is the only method: it calls
CDVQueue::Put(duration, data, len).
This is the handoff from the DirectShow streaming thread to the
application’s own worker thread.
See Threading Model for queue details.
CapturingThread
CDV::CapturingThread() is the consumer.
For each frame dequeued it:
- Calls
GetDVRecordingTime()to extract the camcorder timestamp. - Posts
WM_DV_TIMECHANGEto the parent window if the timestamp changed. - Calls
AnalyzeDVFrame()on the raw frame buffer and accumulates the result withAccumulateErrorStats()intom_errorStatsunderCAutoLock(m_cs). - Calls
CMonitor::HandleFrame()when the queue is below half capacity (preview throttling). - Creates a new
CAVIWriterif none is open or if a split condition is met (frame count limit or DV timestamp discontinuity). - Calls
CAVIWriter::HandleFrame()for everym_everyNthframe. - Updates
m_counter,m_time, and checks the timed capture limit.
Step 3 happens on every frame regardless of whether a CAVIWriter is
open (i.e., even in CapturePaused state when no file is being written).
Disk space monitoring (v1.2.6):
Every ~1500 frames the thread checks free disk space via
GetDiskFreeSpaceEx() on the drive root of m_captureFilename.
If less than 500 MB remains it posts WM_DV_LOWDISKSPACE
(WM_USER + 202) to the dialog so the user can be warned without
interrupting the capture in progress.
End-of-signal auto-stop (v1.2.7):
When CDV::m_autoStopTimeout > 0 (default 5000 ms), the thread uses
CDVQueue::GetWithTimeout() rather than the blocking Get().
A timeout without an EOS marker indicates the FireWire signal was lost.
The thread posts WM_DV_SIGNALLOST (WM_USER + 203), transitions the
state to Finished, and exits cleanly without requiring manual
intervention.
Post-capture phase (v1.6.0):
After the main capture loop exits and CAVIWriter is deleted, the thread
runs a post-capture sequence on the completed file:
AnalyzeDVFrame() per frame (in main loop, under m_cs)
|
v
CheckAVIIntegrity() -- structural RIFF validation
|
v
ComputeFileSHA256() -- if m_enableSHA256 is true
WriteSHA256Sidecar() -- writes <file>.sha256
|
v
WriteCaptureLog() -- appends row to WinDV_CaptureLog.csv
|
v
PostMessage(WM_DV_CHECK_COMPLETE, bCheckPassed, 0)
The UI thread receives WM_DV_CHECK_COMPLETE (WM_USER + 204) and calls
CDVToolsDlg::OnDVCheckComplete(), which reads CDV::GetLastCheckResult()
and CDV::GetErrorStats() to compose the status bar summary.
See Threading Model for the
synchronization details.
CAVIWriter
CAVIWriter is a COutputGraph subclass.
Graph topology – Type-2 AVI (default):
COutputFilter (COutputGraph)
| MEDIATYPE_Interleaved
v
DV Splitter (CLSID_DVSplitter)
| |
| video | audio
v v
AVI Mux (MEDIASUBTYPE_Avi)
|
v
File Sink (IFileSinkFilter2, AM_FILE_OVERWRITE)
Graph topology – Type-1 AVI:
COutputFilter (COutputGraph)
| MEDIATYPE_Interleaved
v
AVI Mux (MEDIASUBTYPE_Avi)
|
v
File Sink
Temporary file strategy:
The AVI file is initially written to a path with a ~ prefix inserted
into the datetime portion of the filename (e.g. clip.~23-01-15_12-30.01.avi).
On destruction, CAVIWriter sends DeliverEndOfStream(), waits up to
5 seconds for EC_COMPLETE (so the AVI Mux writes the file index), then
calls MoveFile() to atomically rename to the final path.
An aborted capture therefore leaves a ~-prefixed file rather than a
corrupt file with its final name.
Record Pipeline: CAVIJoiner to CDVOutput
CAVIJoiner
CAVIJoiner implements CFrameSource and CFrameHandler.
It sequences multiple CAVIReader instances into a single logical stream.
Construction:
- The pipe-delimited
filenamesstring is split on|. - Each token is glob-expanded via
CFileFind. - Each glob result is sorted lexicographically with
qsort/CompareStringsso that split files (clip.01.avi,clip.02.avi) play in the correct order. - All resolved paths are collected into
m_filenames[]. - The first
CAVIReaderis opened immediately soGetMediaType()is available beforeRun()is called.
At runtime, JoinerThread waits on m_ev.
When the current CAVIReader reaches end-of-stream, HandleFrame() is
called with data == NULL, which signals m_ev.
JoinerThread then destroys the old reader and opens the next file.
When all files are exhausted, JoinerThread sends an EOS frame
(duration = -1, data = NULL) to m_joinHandler (which is CDV) and
exits.
CDVOutput
CDVOutput is a COutputGraph + CDVControl subclass.
Graph topology:
COutputFilter (COutputGraph, async queue depth 10)
|
v
DV output filter (FireWire device, friendly name = vdst)
The graph is started immediately in the constructor.
The destructor sends DeliverEndOfStream() and waits up to 5 seconds
for EC_COMPLETE to ensure all queued frames reach the device before
the graph is torn down.
RecordingThread
CDV::RecordingThread() consumes frames from CDVQueue and calls
CDVOutput::HandleFrame() for each frame when m_state == Recording.
While m_state == RecordPaused, frames are consumed but not forwarded.
When CDVQueue::Get() returns false (EOS), the state transitions to
Finished and the thread exits.
AVI Type 1 vs Type 2
DV content can be stored in AVI containers in two ways:
| Feature | Type 1 | Type 2 |
|---|---|---|
| Video stream | Single interleaved stream | Separate vids (DV) stream |
| Audio stream | Interleaved within the DV stream | Separate auds stream |
| File size | Slightly smaller | Slightly larger |
| Compatibility | Some editing apps struggle | Broadly compatible |
| WinDV default | No | Yes (m_type2AVI = true) |
Type-1 capture graph:
COutputFilter --> AVI Mux --> File Sink
The AVI Mux receives the raw interleaved DV stream and stores it as a single video track. Some decoders and editing applications cannot read Type-1 AVI because they expect separate audio and video streams.
Type-2 capture graph:
COutputFilter --> DV Splitter --> AVI Mux --> File Sink
| (video)
+-(audio) -----> AVI Mux
The DV Splitter (CLSID_DVSplitter) separates the interleaved DV stream
into a video stream and one or more audio streams.
The AVI Mux receives both and writes them as separate tracks.
This is the format produced by most DV camcorders’ direct computer
connection and is the most widely supported for editing.
The m_type2AVI flag on CDV is persisted to the registry as
Capture\Type2AVI and toggled from the configuration dialog
(CCaptureCfg).
CAVIReader and the Type-1/Type-2 Fallback
When reading an AVI file back for the Record pipeline, WinDV must accept
both Type-1 and Type-2 files.
CAVIReader handles this with a two-attempt strategy:
Attempt 1 – Type-1 (direct interleaved output):
FileSource --> AVI Splitter --(MEDIATYPE_Interleaved)--> CInputFilter
m_GB->RenderStream(NULL, &MEDIATYPE_Interleaved, pAVI, NULL, m_inputFilter)
succeeds if the AVI Splitter exposes a MEDIATYPE_Interleaved output pin
(a Type-1 file).
Attempt 2 – Type-2 (DV Mux re-interleave):
FileSource --> AVI Splitter --(video)--> DV Mux --> CInputFilter
--(audio)->
If the first attempt fails or leaves CInputPin unconnected, a
CLSID_DVMux is inserted.
The DV Mux recombines the separate video and audio streams back into an
interleaved DV stream before delivery to CInputFilter.
This restores the format that CDV::HandleFrame() expects regardless of
the source file type.
Preview: CMonitor
CMonitor is a COutputGraph subclass that renders frames to a preview
window.
Its topology is built automatically by ICaptureGraphBuilder2::RenderStream()
with a NULL sink:
COutputFilter --> (auto-connected) DV Video Decoder --> Video Renderer
DirectShow selects and connects the appropriate decoder and renderer
automatically.
After graph construction, SetDVDecoding(m_FG, 0) sets the DV Video
Decoder to DVDECODERRESOLUTION_360x240 (half-D1) to reduce CPU load.
The renderer window is embedded as a WS_CHILD subwindow of the
CDV control’s HWND via IVideoWindow::put_Owner().
CMonitor::Resize() recalculates its position to maintain the DV 4:3
aspect ratio whenever the parent control is resized.
Preview delivery is rate-limited by MonitoringThread to approximately
5 fps (200 ms interval).
See Threading Model for the full algorithm.
Error Handling
All DirectShow HRESULT values are tested with the CHECK_HR() inline
function:
inline void CHECK_HR(HRESULT hr,
LPCSTR message = "Error",
int cause = CDShowException::error)
{
if (hr != S_OK) ThrowDShowException(cause, message);
}
ThrowDShowException() allocates a CDShowException (a CException
subclass with b_AutoDelete = TRUE) and throws it via MFC THROW().
All call sites in DVToolsDlg.cpp wrap DirectShow operations in
TRY { ... } CATCH_ALL(e) { ... } END_CATCH_ALL blocks that either
display the error in m_status or call InitVideo() to reset the
pipeline to a known idle state.
CDShowException::m_cause distinguishes between:
| Cause | Value | Meaning |
|---|---|---|
none |
0 | No error (unused) |
deviceNotFound |
1 | Named FireWire device not present |
error |
2 | Generic DirectShow HRESULT failure |