More on DirectShow Filter Graph Spy

Having DirectShow Filter Graph Spy installed in the system, I noticed a new and weird effect that when Media Player Classic plays a list of files and each MP3 file being currently decoded shows decoder’s system tray icon, a switch to a new file leaves an old icon in place and adds a new icon for a new decoder.  I thought this might be also a decoder’s problem which I also recently updated to a new version, but then I noticed other artifacts, with other software and again with Media Player Classic, that if I drop a new file onto the player and it starts playing it, old file is still being played as if player just releases all referenced to the graph and never calls IMediaControl::Stop to force stop of playback.

It was clear at that point that it is related to filter graph spy and the problem needs a fix. The problem definitely looked like the spy leave outstanding references to the filter graph and when the application releases graph and it is supposed to be automatically destroyed, it is still being alive and possibly, such as in case of Media Player Classic, even running.

The first clue was that being published to running object table (ROT), the object gets more than one additional reference on it and as such it never gets removed from ROT until application terminates as ROT keeps a reference to the graph and there is noone to remove it. By original design spy published filter graph on ROT and removed one reference immediately after that to compensate the ROT’s reference (one only) making it a weak reference. So when all references are released from the filter graph but ROT reference, the graph gets destroyed and in FinalRelease class on the COM object we re-add the reference back and automatically remove the object from ROT as well. The whole scheme works in assumption that ROT adds one reference only, if there are more then the object is referenced by the ROT forever.

Debugging however showed that ROT adds a single reference and there is no problem from this side. However I immediately noticed  another weird behavior in the same method FinalConstruct. An underlying (real) filter graph object, i.e. CFilterGraph from quartz.dll, is adding a reference on the outer COM object when being queried for a private interface from implicit (also known as main, raw) IUnknown when created as aggregated object. And it is not exactly the behavior one would expect from aggregated object because it basically break proper reference counting. I realize that aggregating is a rather rare technique and DirectShow COM base is one of the very first COM bases at all which then stopped being developed, so perhaps this problem was not identified on time and later left unfixed at all, so in present situation there should rather be a workaround.

It is also worth mentioning that there are two scenarios involved. My CSpy object creates DirectShow CFilterGraph as an aggregated object. And CSpy in its turn may be created as aggregated too. For example, Windows Media Player creates (as most applications in this universe do) filter graph object the regular way, as a standalone COM object. However it appears that Media Player Classic creates filter graph as aggregated, for whatever it needs it for. Because of the discovered problem inner CFilterGraph adds an outstanding reference on the top level “controlling IUnknown”, which is either CSpy’s private IUnkown in case of Windows Media Player, or higher level IUnknown in case of Media Player Classic.

So the proper workaround is to release unwanted references from controlling unknown rather than self:

CComPtr<IUnknown> pUnknown;
ATLENSURE_SUCCEEDED(pClassFactory->CreateInstance(pControllingUnknown, __uuidof(IUnknown), (VOID**) &pUnknown));
// NOTE: DirectShow FilterGraph is incorrectly implementing COM aggregation adding outer reference to interfaces queried from private IUnknown
CComQIPtr<IFilterGraph2> pFilterGraph2 = pUnknown;
ATLENSURE_THROW(pFilterGraph2, E_NOINTERFACE);
pFilterGraph2.p->Release(); // <<--------------------------

It looked like the problem is finally worked around but in aggregated creation scenario it brought another problem up. Since instantiation of inner CFilterGraph takes place in FinalConstruct and CSpy is also being instantiated as aggregated, it appears that outer COM object is sensible to manipulation with its reference counter. CFilterGraph added a reference it should not, CSpy released it in compensation but on this early stage the compensating Release might zero reference counter and might initiate unexpected object destruction. It is not necessarily this way as it depends on outer COM object base, but it seems that Media Player Classic does not do DECLARE_PROTECT_FINAL_CONSTRUCT or its equavalent and things go the worst scenario.

CSpy is an inner object and we don’t have any external instance to hold a temporary reference for us while outer COM object is being created, so a workround for this is to temporarily hold a circular reference to an outer object to let it complete its instanatiation and pass another reference higher up to the controlling application. At which point we are going to be safe to release the temporary reference and normalize reference counters.

if(m_bIsAggregated)
{
    pControllingUnknown.p->AddRef();
    const ULONG nReferenceCount = pControllingUnknown.p->Release();
    if(nReferenceCount == 1)
        m_pTemporaryUnknown = pControllingUnknown;
}

The only thing is left is to release the temporary reference early enough, but we have a filter graph, don’t we? What is typically done with a filter graph, adding a filter right? And it is a controlling application which has its own reference, who adds filters so we are safe to release temporary reference in IFilterGraph::AddFilter or IGraphBuilder::AddSourceFilter (actually it would not hurt to add this in other methods as well):

STDMETHOD(AddFilter)(IBaseFilter* pFilter, LPCWSTR pszName) throw()
{
    _Z4(atlTraceCOM, 4, _T("pszName \"%s\"\n"), CString(pszName));
    ReleaseTemporaryUnknown(); // <<--------------------------
    return m_pInnerFilterGraph2->AddFilter(pFilter, pszName);
}

Additionally, the spy also implements now IMediaControl interface and traces calls to log file, it is convenient.

A partial Visual C++ .NET 2008 source code is available from SVN, release binary included.

reg-FilterGraphSpy.dll.bat and unreg-FilterGraphSpy.dll.bat files in SVN are convenient batch files to register and unregister the spy with the operating system.

Leave a Reply