Published by Roman on 28 Jan 2009 at 10:46 pm
DirectShow Filter Graph Spy
Inspired by a post on DirectShow Development Forum, it is sometimes useful to find out how certain application is using DirectShow to capture live video and/or audio, or play certain file etc. Filter graph topology, details about media types on the pins, used interfaces and call order. There is quite an easy step for the developer to spy over running filter graphs system wide by connecting Component Object Model (COM) “Treat As” feature with filter graph remoting through Running Object Table and connecting to remote process graphs using GraphEdit utility.
Who could imagine Windows Media Player to build such a filter graph for a video and audio Windows Media file? SRS Wow DMO, equalizer, analyzer and stuff?
There are many tricks to catch the created filter graph but the most legit is the mentioned above “Treat As” feature to emulate certain COM class identifier (CLSID) through another coclass. COM API offers CoTreatAsClass and CoGetTreatAsClass functions to manage the emulation. To emulate DirectShow’s CLSID_FilterGraph through my CSpy class, I am adding the following code to the default COM registration process for the utility DLL:
static HRESULT WINAPI UpdateRegistry(BOOL bRegister) throw() { _Z2(atlTraceRegistrar, 2, _T("bRegister %d\n"), bRegister); _ATLTRY { CLSID TreatAsClassIdentifier; HRESULT nCoGetTreatAsClassResult = CoGetTreatAsClass(CLSID_FilterGraph, &TreatAsClassIdentifier); __C(nCoGetTreatAsClassResult); __D(!bRegister || nCoGetTreatAsClassResult != S_OK || TreatAsClassIdentifier == GetObjectCLSID(), E_UNNAMED); if(!bRegister && nCoGetTreatAsClassResult == S_OK) __C(CoTreatAsClass(CLSID_FilterGraph, CLSID_NULL)); _A(_pAtlModule); UpdateRegistryFromResource<CSpy>(bRegister); if(bRegister) __C(CoTreatAsClass(CLSID_FilterGraph, GetObjectCLSID())); } _ATLCATCH(Exception) { _C(Exception); } return S_OK; }
The idea is that the emulation is enabled in case it was not enabled before or it was already enabled through our coclass. Every time an application cocreates an instance of Filter Graph Manager (FGM), CSpy class will be created instead.
Still as we have no plans to reinvent one of the most important DirectShow objects, we have to embed the original FGM insde to forward the calls to. The proper place in the ATL C++ code is FinalConstruct and what we actually do there is instantiating an aggregated FGM from the original in-process server. The safest would be to query registry for the original host, but we know it’s hosted by quartz.dll and it seems to be safe to hardcode:
HINSTANCE hModule = CoLoadLibrary(_T("quartz.dll"), TRUE); _ATLTRY { typedef HRESULT (WINAPI *DLLGETCLASSOBJECT)(REFCLSID, REFIID, VOID**); DLLGETCLASSOBJECT DllGetClassObject = (DLLGETCLASSOBJECT) GetProcAddress(hModule, "DllGetClassObject"); __E(DllGetClassObject); CComPtr<IClassFactory> pClassFactory; __C(DllGetClassObject(CLSID_FilterGraph, __uuidof(IClassFactory), (VOID**) &pClassFactory)); _A(pClassFactory); CComPtr<IUnknown> pUnknown; __C(pClassFactory->CreateInstance(GetControllingUnknown(), __uuidof(IUnknown), (VOID**) &pUnknown)); CComQIPtr<IFilterGraph2> pFilterGraph2 = pUnknown; __D(pFilterGraph2, E_NOINTERFACE);
What is remained at the point is to implement IFilterGraph2 (along with inherited IGraphBuilder and IFilterGraph) and forward all incoming calls to the inner aggregated object. It would also be helpful to trace calls to a log file C:\FilterGraphSpy.log:
2009-01-28 22:07:51 4456 5656 dllmain.h(29): CFilterGraphSpyModule::CFilterGraphSpyModule: this 0x01cd327c 2009-01-28 22:07:51 4456 5656 Spy.h(75): CSpy::CSpy: this 0x01ce297c 2009-01-28 22:07:51 4456 5656 Spy.h(132): CSpy::AddFilter: pszName "C:\Documents and Settings\John Doe\Desktop\Melissa Cherry Interview.wmv" 2009-01-28 22:07:51 4456 5656 Spy.h(142): CSpy::EnumFilters: ... 2009-01-28 22:07:51 4456 5656 Spy.h(132): CSpy::AddFilter: pszName "ffdshow Audio Decoder" 2009-01-28 22:07:51 4456 5656 Spy.h(152): CSpy::ConnectDirect: ... 2009-01-28 22:07:51 4456 5656 Spy.h(137): CSpy::RemoveFilter: ... 2009-01-28 22:07:51 4456 5656 Spy.h(132): CSpy::AddFilter: pszName "WMAudio Decoder DMO" 2009-01-28 22:07:51 4456 5656 Spy.h(152): CSpy::ConnectDirect: ... 2009-01-28 22:07:51 4456 5656 Spy.h(142): CSpy::EnumFilters: ...
And additionally, the filter graph is immediately published on the ROT and is accessible with GraphEdit. Feel free to spy over graphs system wide.
A partial Visual C++ .NET 2008 source code is available from SVN, release binary included.
- download the binary DLL from version control repository (DirectShowSpy.dll – Win32, x64)
- COM register the DLL on target system, for example from command line using regsvr32 utility “regsvr32 DirectShowSpy.dll” (administrative privilege elevation required on Vista)
- on some operating systems (MS Vista in particular) it is also required to have SDK’s proppage.dll available in the system, see Vista related post for more details
To uninstall:
- use “regsvr32 /u” utility to unregister the DLL, close all DirectShow based applications or reboot OS, and delete the file; see also a related thread on MSDN Forums
Tags: aggregation, ATL, C++, DirectShow, filter, graph, Source, spy
10 Responses to “DirectShow Filter Graph Spy”
Leave a Reply
You must be logged in to post a comment.


alax on 29 Jan 2009 at 20:55 #
A transcript of related chat:
Yes you need to regsvr32 and from this point all new filter graphs should be available on running object table so that you connect to them as to remote graphs using GraphEdit
I don’t think so, but there is another possible cause. Did you at all see any remote graphs using Graph Edit? After upgrading to XP SP3 you might need to run “regsvr32 quartz.dll” to get the ability for GraphEdit to see remote graphs back again
ok, so try regsvr32 quartz.dll from windows\system32 folder and see it this is helpful for both my dll and your apps
if you can’t see your own graphs with graphedit through ROT, then there is definitely some problem… as for my dll, except for ROT you can check generated log file in C:\ProgramData (this is hidden dir) if you see it’s populating then my dll is intercepting filter graph instantiations
OK, there may be something different on Vista (I am using XP but I was pretty sure it is the same way on Vista) or wrong with the system since you don’t see remote graphs
BTW you need to regsvr32 from elevated admin command prompt (this is to make sure).
e.g. there is “CSpy::EnumFilters: …” which means that it actually worked
there is no other way for these lines to appear in the log other than if the graph is hooked by my dll and it is intercepting all filter graphs
BTW if you run MEdia Player Classic, it also publishes its graph on ROT
and with my DLL you will have two entries there, both point to the same graph
alax on 12 Feb 2009 at 08:30 #
From a chat:
First of all, Windows Media Player might be inserting another filter, which other players don’t insert, and this breaks WMP seeking. You can see WMP’s filter graph using my Filter Graph Spy utility.
A second possible reason is that you don’t fully (for some reason) implement seeking, in which case you should have some weird method called on your filter, what other players don’t do. Or – another interface is queried from your filter, you don’t implement it and return E_NOINTERFACE and that’s all, you don’t receive other calls.
alax on 29 Mar 2009 at 23:51 #
I have not published them, please use binary – it is also in SVN.
alax on 29 Apr 2009 at 12:11 #
regsvr32 /u FilterGraphSpy.dll
from command line, then try to delete file (if it is still locked, reboot and delete after that). If this does not help, you have other issues in your system that result in your inability to debug.
sdg78p on 02 Jun 2009 at 19:46 #
Several of your projects include “roatltrace.h” your replacement for . Where is this file, or what are the differences between yours and the original?
alax on 02 Jun 2009 at 21:26 #
This my file I never yet published.
sdg78p on 01 Jul 2009 at 02:00 #
I’m making a filter that uses TreatAs to hook an existing IBaseFilter derived filter using the same approach as your example (load lib, reflect calls, etc.), and it all works great as long as I return the inner filter’s IBaseFilter (and its base interfaces) when queried (all other queries are passed to the inner object).
I.e. this works:
…
COM_INTERFACE_ENTRY_FUNC(IID_IBaseFilter, NULL, CFilter::GetBaseFilter)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnknown)
…
static HRESULT WINAPI GetBaseFilter(void* pv, REFIID riid, LPVOID* ppv, DWORD dw) throw()
{
CFilter* pFilter = reinterpret_cast(pv);
*ppv = m_pInnerBaseFilter;
return S_OK;
}
But if I return the reflecting IBaseFilter interface when queried, it works fine in GraphStudio and GraphEdit, but crashes in WMPlayer when the graph is released.
I.e. this doesn’t work:
…
COM_INTERFACE_ENTRY(IBaseFilter)
COM_INTERFACE_ENTRY(IMediaFilter)
COM_INTERFACE_ENTRY(IPersist)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnknown)
…
// reflected interface methods…
STDMETHODIMP EnumPins(IEnumPins **ppEnum)
{ return m_pInnerBaseFilter->EnumPins(ppEnum); }
… etc.
In both cases, I’m using the same mechanism to load and query the interface on the inner filter object in FinalContruct:
CComQIPtr pBaseFilter = pUnknown;
pBaseFilter.p->Release();
m_pInnerUnknown = pUnknown;
m_pInnerBaseFilter = pBaseFilter;
It look like a refcount is off, but I’ve tried bumping the counts to avoid a delete, and it still crashes. Btw, when I bump the counts, GraphEdit reports unreleased objects.
Do you have any insights as to why the filter might work fine in one app, but not in another?
Also, why does your CSpy reflect the IMediaControl interface? Is this just to get more info, or is there another reason?
alax on 01 Jul 2009 at 07:57 #
With an access violation during destruction, the most likely cause is reference counting. You should probably see how you are destroying your object and then receive another call on its interface. And when you return original IBaseFilter you probably don’t have any reflecting code remained, other than instantiation and QueryInterface. Are you aggregating the filter you are hooking? Basically BaseClasses are not 100% COM compliant (they are coming from long ago and nobody cared to fix this compliance since then).
So if you are aggregating, the might be some [small] issues in COM aggregation compliance, and if you are not aggregating, or the filter you are hooking does not support aggregation, then you must hook all interfaces of interest so that you never expose original one, including those of pins.
The difference between applications might be in an order of calls. For example, some application can obtain pin’s interface and then through it get owner filter interface and keep it so that it is released the last. If you are not aggregating, this can lead to situation when you already have no interface pointers outstanding and your filter is destroyed, while application still hold a reference to your inner object.
It is not essential to hook this interface for ROT stuff, but additionally to ROT the spy logs all graph calls to log file and here is where it is also convenient to log IMediaControl method calls: Run, Stop etc.
sdg78p on 09 Jul 2009 at 08:44 #
Thanks for the suggestions. It was a refcount issue due to the filter being held by its pins. I had to catch all the points where the inner filter was being returned and handle those interfaces too. Works great now.
Roman on 24 Dec 2009 at 15:11 #
Note that the binary was renamed from FilterGraphSpy.dll to DirectShowSpy.dll.