转载 Extending KS Proxy with Custom Property Pages and Interface Plug-ins November 26, 2003

来源:互联网 发布:淘宝网第一家网店 编辑:程序博客网 时间:2024/05/05 08:21

Extending KS Proxy with Custom Property Pages and Interface Plug-ins

Extending KS Proxy with Custom Property Pages and Interface Plug-ins
November 26, 2003
Max Paklin

Copyright (C) 2003 Max Paklin. All rights reserved.

 

Microsoft Kernel Streaming (KS) technology has been around since Windows 98 Gold. It has developed into a mature technology that the DDK describes very well. Still, some parts of it remain a mystery to many people trying to build multimedia drivers for the PC market. This situation may exist because Microsoft decided that certain parts of technology wouldn't be widely used and therefore didn't deserve the attention needed to describe them in detail.

One of the subjects that slipped by the KS document writers is KS Proxy customization and extension. This article and associated samples attempt to fill the void in the DDK and give a good starting point to those writing multimedia drivers based on KS.

What is KS Proxy?

Every kernel minidriver that is based on KS, be it Stream Class or AVStream minidriver, uses the KS proxy (ksproxy.ax) as a user-mode surrogate. KS Proxy is a DirectShow filter that allows seamless integration of the objects created by the minidriver into the DirectShow streaming environment. When registering with KS, the minidriver gives the class driver the description of the objects that the minidriver wants to expose. The class driver reports those objects to user mode. KS Proxy is the primary consumer of that information. Based on the description of objects provided by the minidriver, KS Proxy creates one or more filters that represent the minidriver in DirectShow. It also creates connection points called "pins"  to allow the filters to connect with other filters. Pins serve as as the endpoints of streaming pipes.

The proxy technique allows the minidriver writer to avoid having to explicitly expose the device and its capabilities to DirectShow. While this is all good, it does pose one problem. Many devices have unique capabilities that are not covered by the existing Windows infrastructure. This is especially so in multimedia land. Fortunately Microsoft planned for this and designed KS Proxy in such a way that it can be easily extended and customized.

Introduction to KS Proxy Plug-ins

The key component  that allows you to customize KS Proxy is the "KS Proxy Plug-in". A plug-in is a simple COM object that you registered with KS Proxy by entering specific information in the registry. By looking into predefined registry location, KS Proxy can discover all the plug-ins that have been installed on the system.

In this article I will be focusing on two types of plug-in: Property Page and Interface Handler plug-ins. A property page is the first obvious thing that comes to mind when a developer wants to provide some user interface for a property or setting that is unique to his or her device. 

While this is certainly a good starting point, displaying a property page may not be good for every scenario. This is especially true when we consider a large end-user application. The application writers usually have their own ideas on what is the most convenient way for the user to gain access to a particular device property. Therefore, there is a clear need for an application writer to be able to have the same level of access to custom device properties without having to display the user interface of a property page. An interface handler plug-in fills this need. An interface handler is a COM object that provides access to device properties through COM interfaces that application developers are accustomed to.

On top of abovementioned plug-ins there are other types. For example, a "type handler" plug-in translates KS samples into DirectShow media samples for specific media types. Those plug-ins are beyond the scope of this article.

Getting started

The sample programs that accompany this article (see http://www.wd-3.com/downloads/Wd3KsProxyPluginSample.zip)  include three distinct pieces. The first piece is a simple driver (wd3ksdrv.sys and associated INF) that exposes a custom property. The second piece of the puzzle is a plug-in (Wd3ProxyPlugin.ax) containing one property page to give access to the custom property exposed by the driver and an interface to let an application control the property in question; this module is both a property-page plug-in and an interface handler plug-in. Finally, I've included a trivial MFC application that provides basic user interface to control the property and the DirectShow graph that the driver participates in.

The samples require Windows 2000 or above. To test the samples on a Windows 2000 machine, you should install the DirectX 9 runtime. Theoretically, the samples should work fine without DirectX 9 on Windows XP and above, but I didn't test them. Everything has been tested on Windows 2000 Pro, Windows XP Pro SP1 (both checked and free builds on single and dual processor machines) and Windows 2003 Standard Server with the DirectX runtime installed.

To build the driver, you will need the latest and greatest DDK. I used the Windows XP or Windows Server 2003 build environments provided by DDK build 3790. Note that the driver will not build in the Windows 2000 environment; it will, however, run on Windows 2000 just fine. You will also need to install the DirectX SDK, version 8.1 or above.

To build the plug-in project, the DirectShow SDK is a must. Here I am not talking about DirectShow headers and libraries. I am talking about the real SDK.

To compile the plug-in sample, you need to define two environment variables:

MSSDK=E:/MSSDK
BASECLASSES=E:/MSSDK/Samples/Multimedia/DirectShow/BaseClasses

MSSDK is the root of either Windows Platform SDK with DirectX SDK included or the (separate) DirectX SDK.

Both the plug-in and the test application projects require MS Developer Studio.NET 2003.

To install the driver, copy wd3ksdrv.inf, wd3ksdrv.sys and Wd3ProxyPlugin.ax into a separate directory, then right-mouse-click on the INF file in Windows Explorer and select Install. That will invoke the setup program to process the DefaultInstall section of the INF to install the driver and the plug-in. Make sure that the required runtime libraries (for example, msvcr71.dll) are present on the target machine before you run the installation. Otherwise the plug-in will fail to register. Hint: Copy the plug-in DLL to the system32 directory by hand and run the DEPENDS utility that comes with Visual Studio.

A walk through the sample driver code

The sample driver is a virtual software driver enumerated by SWENUM bus. It is similar to the avshws sample shipped with the DDK (I cut and pasted some boilerplate parts of avshws, in fact), except that it is much simpler.

The driver exposes one filter in the WDM Streaming Capture Devices category. The filter serves as a capture source and has one output pin. The format of the capture pin is RGB24. When rendered, the pin connects to the standard video renderer either directly or through the Color Space Converter filter. The exact connection path depends on the graphics card and/or on current video mode. You needn't worry about these connection details, since the DirectShow graph builder will automatically figure out how to render the output pin.

Let's see what it takes to add a property to a driver. In this particular example we will have a filter that exposes the frame rate property. That property specifies how many frames per second the capture pin sends out.

First, you need to define a globally unique identifier (GUID) for a property set. I used the guidgen tool provided with the Platform SDK or MS Developer Studio and copied the resulting GUID into the header file wd3prop.h.

#define STATIC_KSPROPERTYSET_Wd3KsproxySample /
    0xc6efe5eb, 0x855a, 0x4f1b, 0xb7, 0xaa, 0x87, 0xb5, 0xe1, 0xdc, 0x41, 0x13
DEFINE_GUIDSTRUCT( "C6EFE5EB-855A-4f1b-B7AA-87B5E1DC4113", /
    KSPROPERTYSET_Wd3KsproxySample );
#define KSPROPERTYSET_Wd3KsproxySample /
    DEFINE_GUIDNAMED( KSPROPERTYSET_Wd3KsproxySample )

A property set defines a collection of properties that have something in common. A property item is an individual property in a property set. Conventionally, you use an enumerated type to assign numeric values to the property items in a set. In this example, I defined a single property:

typedef enum
{
    KSPROPERTY_WD3KSPROXYSAMPLE_RATE
} KSPROPERTY_WD3KSPROXYSAMPLE;

The mechanics of handling a property in a KS filter driver are very complex. Rather than implement these mechanics myself, I do what most people do and define table entries that KS will use to do most of the work for me. The key pieces that I need to supply myself are get handler and set handler subroutines. In order to create the necessary table entries at the appropriate spot in the driver, it's convenient to define a macro like this one:

#define DEFINE_KSPROPERTY_WD3KSPROXYSAMPLE_RATE( GetHandler, SetHandler )/
    DEFINE_KSPROPERTY_ITEM(/
        KSPROPERTY_WD3KSPROXYSAMPLE_RATE,/
        (GetHandler),/
        sizeof( KSPROPERTY ),/
        sizeof( ULONG ),/
        (SetHandler),/
        NULL,/
        0,/
        NULL,/
        NULL,/
        0 )

We associate a property with the filter in an optional object called an automation table. The automation table provides the information about all properties, methods and events that the filter supports. In our simple example, we aren't supporting any methods or events, so we define our automation table this way:

DEFINE_KSAUTOMATION_TABLE( c_autTable )
{
    DEFINE_KSAUTOMATION_PROPERTIES( c_psTable ),
    DEFINE_KSAUTOMATION_METHODS_NULL,
    DEFINE_KSAUTOMATION_EVENTS_NULL
};

c_psTable is the name of an array that defines the property sets supported by the filter:

DEFINE_KSPROPERTY_SET_TABLE( c_psTable )
{
    DEFINE_KSPROPERTY_SET(
    &KSPROPERTYSET_Wd3KsproxySample,
    SIZEOF_ARRAY( c_piWd3ProxySample ),
    const_cast< PKSPROPERTY_ITEM >( c_piWd3ProxySample ),
    0,
    NULL )
};

The property set is identified by a property set GUID, which we defined earlier. It contains a pointer to the property item table c_piWd3ProxySample, which describes all items from a particular property set that this filter supports. In our case the array of property items has only one element:

DEFINE_KSPROPERTY_TABLE( c_piWd3ProxySample )
{
    DEFINE_KSPROPERTY_WD3KSPROXYSAMPLE_RATE(
        CCaptureFilter::ProcessProperty,
        CCaptureFilter::ProcessProperty )
};

Note that we actually have to define these table in reverse of the order I've described them so that the compiler sees each of the names (c_piWd3ProxySample, for example) before any references to the name.

I decided to use a single function (CCaptureFilter::ProcessProperty) as both the get and set handler, by the way. I'll show you later how to tell which role the function is supposed to play during a particular call. Here is the prototype of this routine:

NTSTATUS ProcessProperty( PIRP pIrp, PKSIDENTIFIER pKsIdentifier, PVOID pData );

It receives an original IRP, a KSIDENTIFIER structure that describes the property being set or queried, and a pointer to a data buffer. The IRP will be an IRP_MJ_DEVICE_CONTROL. KS will validate the parameters of the IRP by reference to the automation table so that, for example, we can be sure that whoever issued the IRP supplied an input or output buffer that's long enough to hold the ULONG value that we expect for our custom property.

if( pKsIdentifier->Flags & KSPROPERTY_TYPE_SET )
    ntStatus = pFilter->SetRate( *plRate );
else
{
    ULONG ulSize = IoGetCurrentIrpStackLocation( pIrp )
        ->Parameters.DeviceIoControl.OutputBufferLength;
    if( ulSize < sizeof( long ) )
        ntStatus = STATUS_BUFFER_TOO_SMALL;
    else
    {
        *plRate = pFilter->GetRate();
        pIrp->IoStatus.Information = sizeof( long );
    }
}

Note that I double check the length of the output buffer to make sure it's big enough. This isn't, strictly speaking, necessary because KS has already done this length check. Still, I feel it's better to be safe than sorry.

The sample filter uses the frame rate property to control how often it generates an output frame. The generated samples change color from black to white in approximately 25 steps. Hence, when running the graph the content of the Video Renderer's window slowly changes color from black to white depending on the selected frame rate.

To handle the output mechanics, I create a worker thread when the stream transits from the paused state to the running state, and I destroy the thread when the stream transits back to the paused state. The mechanics of creating this thread are no different than in any other kernel driver. If you download and install the sample code, you'll find these mechanics in the file named CAPTURE.CPP that forms part of the driver sample.

The worker thread spends most of its life waiting for a timeout based on the value of the frame rate property or for a "kill streaming" event to occur. It sends a sample when the wait times out. During the transition from the running state back to the paused state the pin sets the event and that causes the worker thread to exit.

for( ;; )
{
    long lRate = pFilter->GetRate();
    // Make sure that lRate is non-zero...
    LARGE_INTEGER liTimeout;
    liTimeout.QuadPart = -10000000/lRate;

    ntStatus = KeWaitForSingleObject( &m_keStopPin, Executive, KernelMode, FALSE, &liTimeout );
    if( ntStatus !=
STATUS_TIMEOUT )
        break;
    // Fill the buffer with data...
}

The only other thing worth mentioning about the capture pin is the behavioral model it implements. AVStream pins can be notified when a new buffer has arrived, when a new buffer has arrived at a time when the sample queue is empty, or never. In this example, the pin defines its behavior as follows:

KSPIN_FLAG_FIXED_FORMAT |
    KSPIN_FLAG_PROCESS_IN_RUN_STATE_ONLY |
    KSPIN_FLAG_DO_NOT_INITIATE_PROCESSING,

The literal meaning of these flags seems a bit strange. On the one hand, I'm saying, "I want to process data in the running state only." On the other, I ask KS to not initiate processing at all. The reason behind this is what is called prefetching in the world of video capture. When the stream transitions to the paused state, the Video Renderer (and some other filters) can direct the source to fill the sample queue so that the playback, once it starts, is free from glitches. By specifying KSPIN_FLAG_PROCESS_IN_RUN_STATE_ONLY, the pin says that it does not want to do prefetching and it can stream only in the running state.

Specifying KSPIN_FLAG_DO_NOT_INITIATE_PROCESSING means that KS will not notify the pin every time a new buffer arrives in the queue. In our case, this flag is particularly useful because we always have data available (it is synthesized, after all), while, at the same time, we want to send the next sample out based on our own timer.

About plug-ins

I want to show you that writing a KS Proxy plug-in is very simple. Let's start with an interface handler plug-in.

The sole purpose of the interface plug-in is to map a driver-specific property set onto a COM interface. Dealing with KS properties is too arcane, and way too inconvenient, for application writer. In contrast, dealing with COM interfaces is something that experienced application developers take in stride. So, for the custom property set implemented by the sample, I defined a simple COM interface that the interface handler will expose. Here's the IDL code for the interface:

[
    object,
    uuid( C6EFE5EB-855A-4f1b-B7AA-87B5E1DC4113 ),
    pointer_default(unique)
]
interface IWd3KsproxySampleConfig : IUnknown
{
    HRESULT GetRate( [out, retval] long* plRate );
    HRESULT SetRate( [in] long lRate );
};

An interface handler is a simple COM object that is aggregated with KS Proxy upon creation. Aggregation is the only way for the object to interact with KS Proxy for driver communication. This is how it is done in CWd3ProxyPluginIFace::CreateInstance:

if( piOuterUnknown == NULL )
{
    DbgLog(( LOG_ERROR | LOG_TRACE, 0, _T( "No outer unknown object passed to interface handler!" ) ));
    hResult = VFW_E_NEED_OWNER;
}
else
{
    IKsPropertySet* piKsPropertySet;
    hResult = piOuterUnknown->QueryInterface(
                __uuidof( piKsPropertySet ),
                reinterpret_cast< void** >( &piKsPropertySet ) );
    ...
}

First, we insure that the outer object is non-zero. Then we query it for the IKsPropertySet interface. That's the interface that we will use to talk to our driver. IKsPropertySet is one of many interfaces supported by KS Proxy. The other interface of some interest is IKsObject. The only method of IKsObject is KsGetObjectHandle, which returns a  file handle that can be used to make DeviceIoControl calls to the underlying pseudo-device. Having acquired the handle, it is possible to issue more complex requests down to the device using KsSynchronousDeviceIoControl (which internally calls DeviceIoControl). Querying minimum or maximum values for a given property, for example, is something that cannot be handled with IKsPropertySet. For the simple example I'm showing in this article, IKsPropertySet is more than enough, though, so we won't bother querying for the IKsObject interface.

After we get the IKsPropertySet interface, talking to the driver is as simple as calling a method of IKsPropertySet. For example, to set the property, here's all we need to do:

STDMETHODIMP CWd3ProxyPluginIFace::SetRate( long lRate )
{
    HRESULT hResult;
    hResult = m_piKsPropertySet->Set(
                KSPROPERTYSET_Wd3KsproxySample,
                KSPROPERTY_WD3KSPROXYSAMPLE_RATE,
                NULL,
                0,
                &lRate,
                sizeof( lRate ) );
    // Error handling...
    return hResult;
}

Finally, we need to set certain registry values so that KS Proxy will be aware of our interface plug-in. Remember that the plug-in is a regular COM component and, because of that, must be registered just like any other COM server. When registered with COM, the plug-in becomes accessible to the rest of the system through COM services. The plug-in must also register itself with KS Proxy as a provider of KS Proxy extensions. The standard way to accomplish registration is to provide exported entry points named DllRegisterServer and DllUnregisterServer in the DLL that implements the COM object. (Look in the file named Wd3ProxyPlugin.cpp.) You can use the system utility named REGSVR32 to invoke these entry points while you're testing your plug-in. To handle registration during an end-user install that includes a driver package, you can include a RegisterDlls directive in the install section of your INF file. (Using a RunOnce value to invoke REGSVR32, as I did in the sample package, is not recommended because it requires a client-side install and, hence, creates a WHQL test failure.) Since installing the DLL via the INF file means that your DLL will be covered by the digital signature you receive from WHQL, and since any modifications you make to the DLL will therefore require recertification, you may find it more convenient to write a co-installer DLL that will handle the registration mechanics. Explaining all these details is beyond the scope of this article.

The HKLM/System/CurrentControlSet/Control/MediaInterfaces/[Property Set]/ key describes the mapping between a property set and the interface that implements it. When an instance of KS Proxy is created, it walks through that list to builds a mapping of property sets to interface IIDs. If KS Proxy is later queried for the interface corresponding to a specified IID, it creates an instance of the associated COM object using the property set GUID as the CLSID of the COM object. (COM does not prevent us from using the same GUID for a property set, the CLSID of our component and the IID of the interface that the component supports. We take advantage of this license to use our one GUID in these three ways. Had I wanted to, I could have run guidgen twice so as to create separate GUIDs for the interface and class/property-set identifiers.

You should end up with these registry settings:



The mapping of property sets to property pages is in the MediaSets branch of the registry, under HKLM/System/CurrentControlSet/Control/MediaSets/[Property Set]/PropertyPages. For each property page associated with a property set there is a separate sub key. This allows you to create multiple property pages for a single property set.

This is what the registry looks like in our example:



A property page for KS Proxy is just a regular COM property page. Like any other COM property page, it supports the IPropertyPage interface. The DirectShow library contains the base class CBasePropertyPage, which helps a little to hide the implementation details of IPropertyPage and provides a simpler interface.

An easy way to use the property page plug-in is to use the GraphEdit tool that comes with the DirectShow SDK. Let's start with opening GraphEdit and inserting the filter for our device driver. From the Graph menu, select Insert Filters. That will open a dialog box listing all the filters installed on the machine. Open the WDM Streaming Capture Device category, select the Wd3ksdrv Source filter and hit the Insert Filter button. These steps will create an instance of KS Proxy for our device. Right-click on the filter's output pin and select Render from the resulting popup menu to build the rest of the test graph. Once built, the graph can be started using the Play button on the toolbar.

Now let's create an instance of our property page plug-in. To do that click on the filter with right mouse button and select Properties from the menu. On the property page you can see the property that the device exposes.

GraphEdit creates an instance of its own property frame rather than a standard COM property sheet. It passes a pointer to the IUnknown interface on a KS Proxy object to it. The property frame calls the standard QueryInterface method on that interface to get a pointer to an ISpecifyPropertyPages interface. Then it calls ISpecifyPropertyPages::GetPages. KS Proxy implements GetPages by looking up through the property set table that the underlying device reported. For each property set it then looks in the registry to see if there is a page or pages registered for the set. KS Proxy thus builds a list of property page CLSIDs, which it returns as the result of the GetPages call.

This is how the property frame finds out what pages it will display. For each CLSID in the list, the property frame then calls CoCreateInstance to create an instance of the page object. It then calls each page's IPropertyPage::SetPageSite function. The argument to SetPageSite is the IUnknown interface on the KS Proxy object passed to it. Thus, each page establishes a connection to KS Proxy and thence to its associated driver.

When the property sheet that is hosting the property page is ready to initialize the page, it calls IPropertyPage::SetPageSite, which translates into CBasePropertyPage::OnConnect. What the page gets as the parameter to that call is the interface to the object, which the property page applies to. In our case this object is KS Proxy. Once the connection with KS Proxy is made the rest becomes easy. If there was no interface handler, then the property page could query the received interface for IKsPropertySet and take it from there. In our case it is even easier. The page simply queries for IWd3KsproxySampleConfig, which is the interface implemented by our interface handler. Armed with IWd3KsproxySampleConfig the property page has all it needs to set or get the property.

HRESULT CWd3ProxyPluginPage::OnConnect( IUnknown* piUnknown )
{
    HRESULT hResult;
    if( piUnknown == NULL )
        hResult = VFW_E_NEED_OWNER;
    else
    {
        hResult = piUnknown->QueryInterface(
                    __uuidof( m_piConfig ),
                    reinterpret_cast< void** >( &m_piConfig ) );
    }
    return hResult;
}

The rest is regular Win32 UI programming. The sample page sets the property on the driver when the user presses Apply button. That runs CBasePropertyPage::ApplyChanges method.

HRESULT CWd3ProxyPluginPage::OnApplyChanges()
{
    HRESULT hResult = m_piConfig->SetRate( m_lRate );
    if( FAILED( hResult ) )
        DisplayError( IDS_ERROR_SETRATE, m_lRate, hResult );
    else
    {
        m_bDirty = FALSE;

        if( m_pPageSite != NULL )
            m_pPageSite->OnStatusChange( PROPPAGESTATUS_CLEAN );
    }
    return hResult;
}
 

While being destroyed, the property frame uses IPropertyPage::SetPageSite( NULL ) to disconnect each page from KS Proxy. That is the time for the page to let go the reference it holds on KS Proxy.

HRESULT CWd3ProxyPluginPage::OnDisconnect()
{
    if( m_piConfig != NULL )
    {
        m_piConfig->Release();
        m_piConfig = NULL;
    }
    return S_OK;
}

Test application

The sample package for this article include the test application. It is very simple MFC based thing that is used to demonstrate basic concepts associated with using plug-ins in an application.



To build the graph, it uses IGraphBuilder. It first adds the source filter, which is the KS Proxy object for our capture driver. To locate our driver it uses the system device enumerator. The enumerator implements IEnumMoniker,which is how the test application finds the device it is interested in. The application simply goes through the list, instantiates the object for each item and queries it for IWd3KsproxySampleConfig.

hResult = piMoniker->BindToObject(
            NULL,
            NULL,
            __uuidof( piFilter ),
            reinterpret_cast< void** >( &piFilter ) );
if( SUCCEEDED( hResult ) )
{
    hResult = piFilter->QueryInterface(
                __uuidof( m_piConfig ),
                reinterpret_cast< void** >( &m_piConfig ) );
    if( FAILED( hResult ) )
        piFilter->Release();
    else
    {
        m_piCaptureFilter = piFilter;
        ...
        m_piConfig->SetRate( m_lRate );
        ...
    }
}

Once it succeeds, the application knows that the search is over and the filter represents the device it has been looking for. In that case the returned pointer is our own interface handler. The rest is again plain Win32 UI programming.

One more thing to mention is that, by clicking the Properties button, the user can create a property sheet with all the property pages supported by the device. Our device has just one, of course. The mechanics of creating the property sheet are very similar to what was described in the previous section.

The last feature worth mentioning is the Batch button. In my opinion, a "batch" mode is the only major feature missing from GraphEdit. With all PnP and power management issues handled by the class driver, the only major implementation problems that a driver programmer faces (if you leave aside surprise removal, that is) are to correctly implement state transitions to avoid resource leaks. Most of the bugs in streaming devices drivers occur during application startup and/or shutdown. I added a simple variant of batch mode testing by continuously running state transitions and graph initialization/uninitialization sequences. The parameters of batch mode are hard coded, but can be easily changed or even exposed though UI.

Sample package

The sample package that includes the driver, the KS Proxy plug-in module and the test application is available at http://www.wd-3.com/downloads/Wd3KsProxyPluginSample.zip.

About the author

Max Paklin is a software engineer located in Silicon Valley, CA. He is working for Digital Keystone, Inc., a startup company building solutions for bridging personal computer, consumer electronics and content industries. He can be reached at mpaklin@hotmail.com.