5,442,164 members and growing! (18,187 online)
Email Password   helpLost your password?
Multimedia » Audio and Video » Video     Intermediate License: The Code Project Open License (CPOL)

DirectShow: TV Fine-tuning using IKsPropertySet in C#

By almere109

Enhancements to the DirectX.Capture class for TV fine-tuning using IKsPropertySet
C#, Windows, .NETVS2005, Visual Studio, Dev

Posted: 30 Jan 2007
Updated: 23 Feb 2008
Views: 49,145
Bookmarked: 44 times
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
10 votes for this Article.
Popularity: 4.11 Rating: 4.11 out of 5
1 vote, 10.0%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
2 votes, 20.0%
4
7 votes, 70.0%
5
Sample image

Introduction

This article is a follow-up of my two previous articles Audio File Saving for the DirectX.Capture Class and Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library. Those articles describe how to do file saving for captured audio and video. This article will show a solution for getting a specific broadcast station by selection of a specific station name or just entering the broadcast frequency to get the audio and video to be captured. Associated with this article, you can find an extra code example with some interesting features such as the FM Radio, VMR9, de-interlacing and SampleGrabber.

Background

The article that gave me the basic idea was DirectShow - Fine TV Tuning using IKsPropertySet written by Liaqat Fayyaz.

When I started this project I thought, "What is happening here?" Step-by-step I discovered what was happening and I discovered that translating about 70 lines of C code into working C# code was not that easy! I had to understand the meaning of much DirectShow-specific code. It was quite difficult to find usable information. The Liaqat Fayyaz article helped me find the proper terms and then, with much thought, I discovered how to use the unmanaged data in C# without getting errors. Finally, it took 300 lines of C# code to make TV fine-tuning work. The count of 300 lines is because there is also some "invisible C code" needed that is in some DirectX SDK include files. To understand everything, I had to visit MSDN many many times.

The normal way of selecting a TV channel or a TV broadcast station is to choose a channel number between 1 and 368 (for the Netherlands, Europe). These channel numbers are based on a pre-defined frequency table, with frequencies between 45 and 863 MHz. The exact frequencies depends on the country, video standard (Pal, Ntsc, Secam) and the TV tuner. Via the IAMTuner interface and the put_Channel method, the desired TV channel can be chosen.

Still, there is the problem that not every TV broadcast station can be tuned. This is the main reason why I prefer the solution of just choosing one of the frequencies my cable provider offers me. Simply enter the frequency, do a little fine-tuning and then view the TV program. Happily, the IKsPropertySet interface of the TV tuner object offers this solution.

Using the Code

To understand the C# code, you must understand the original C code that I used as a starting point. This code can be found in Liaqat Fayyaz's article DirectShow - Fine TV Tuning using IKsPropertySet. Based on this article, I wrote a C# solution. We'll start with some original C code and a short explanation. Looking to the next macro you will probably think, "Wow, what is happening here?" Well, that was at least what I thought.

#define INSTANCEDATA_OF_PROPERTY_PTR(x) ((PKSPROPERTY((x))) + 1)

This is a pointer to the actual data that is used as input and/or returned as output.

#define INSTANCEDATA_OF_PROPERTY_SIZE(x) (sizeof((x)) - sizeof(KSPROPERTY))

This is the size of the actual property data structure that is used for input and output.

hr = m_pTvtuner->QueryInterface(IID_IKsPropertySet, 
    (void**)&m_pKSProp);

QueryInterface does a query for the interface pointer of an object, in this case the TV tuner object. Without having such interface, there is nothing to do.

KSPROPERTY_TUNER_MODE_CAPS_S ModeCaps;
KSPROPERTY_TUNER_FREQUENCY_S Frequency;
memset(&ModeCaps,0,sizeof(KSPROPERTY_TUNER_MODE_CAPS_S));
memset(&Frequency,0,sizeof(KSPROPERTY_TUNER_FREQUENCY_S));

This shows property data structures and memory allocation for the TUNER_MODE_CAPS_S and TUNER_FREQUENCY_S properties used in the SetFrequency() function. The TUNER_MODE_CAPS_S property data structure provides the capabilities of TV tuner devices. Important information that is needed is at least the minimum frequency and maximum frequency that can be tuned. The TUNER_FREQUENCY_S property data structure is used for setting the frequency.

hr = m_pKSProp->QuerySupported(PROPSETID_TUNER, KSPROPERTY_TUNER_MODE_CAPS,
                              &dwSupported);

QuerySupported checks if it is possible to use the get or set methods for a specific property.

if(SUCCEEDED(hr) && dwSupported&KSPROPERTY_SUPPORT_GET)
{
    DWORD cbBytes=0;
    hr = m_pKSProp->Get(PROPSETID_TUNER,KSPROPERTY_TUNER_MODE_CAPS,
        INSTANCEDATA_OF_PROPERTY_PTR(&ModeCaps),
        INSTANCEDATA_OF_PROPERTY_SIZE(ModeCaps),
        &ModeCaps, sizeof(ModeCaps), &cbBytes);  
}
else
    return E_FAIL;

TUNER_MODE_CAPS_S property data is used to initialize the tuning flags. The new frequency is copied into the TUNER_FREQUECY_S property data.

Frequency.Frequency=Freq;
if(ModeCaps.Strategy==KS_TUNER_STRATEGY_DRIVER_TUNES)
    Frequency.TuningFlags=KS_TUNER_TUNING_FINE;
else
    Frequency.TuningFlags=KS_TUNER_TUNING_EXACT;

The new frequency is validated. If the frequency is within the range, the frequency change will be sent to the TV tuner object via the set method.

if(Freq>=ModeCaps.MinFrequency && Freq<=ModeCaps.MaxFrequency)
{
    hr = m_pKSProp->Set(PROPSETID_TUNER,
        KSPROPERTY_TUNER_FREQUENCY,
        INSTANCEDATA_OF_PROPERTY_PTR(&Frequency),
        INSTANCEDATA_OF_PROPERTY_SIZE(Frequency),
        &Frequency, sizeof(Frequency));
    if(FAILED(hr))
        return E_FAIL;  
}
else
    return E_FAIL;

If everything goes as expected, the new TV frequency changes and the TV channel shows up. If the previous code is not complete, the following code must be taken into account too! The code shown comes from strmif.h, ks.h and ksmedia.h in the DirectX SDK.

MIDL_INTERFACE("31EFAC30-515C-11d0-A9AA-00AA0061BE93")
IKsPropertySet : public IUnknown
{
public:
    virtual /* [local] */ HRESULT STDMETHODCALLTYPE Set( 
            /* [in] */ REFGUID guidPropSet,
            /* [in] */ DWORD dwPropID,
            /* [size_is][in] */ LPVOID pInstanceData,
            /* [in] */ DWORD cbInstanceData,
            /* [size_is][in] */ LPVOID pPropData,
            /* [in] */ DWORD cbPropData) = 0;
        
    virtual /* [local] */ HRESULT STDMETHODCALLTYPE Get( 
            /* [in] */ REFGUID guidPropSet,
            /* [in] */ DWORD dwPropID,
            /* [size_is][in] */ LPVOID pInstanceData,
            /* [in] */ DWORD cbInstanceData,
            /* [size_is][out] */ LPVOID pPropData,
            /* [in] */ DWORD cbPropData,
            /* [out] */ DWORD *pcbReturned) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE QuerySupported( 
            /* [in] */ REFGUID guidPropSet,
            /* [in] */ DWORD dwPropID,
            /* [out] */ DWORD *pTypeSupport) = 0;
};

typedef struct 
{
    GUID  Set;
    ULONG  Id;
    ULONG  Flags;
}

KSIDENTIFIER;

typedef KSIDENTIFIER KSPROPERTY;

typedef struct 
{
    KSPROPERTY Property;
    ULONG  Frequency;                   // Hz
    ULONG  LastFrequency;               // Hz (last known good)
    ULONG  TuningFlags;                 // KS_TUNER_TUNING_FLAGS
    ULONG  VideoSubChannel;             // DSS
    ULONG  AudioSubChannel;             // DSS
    ULONG  Channel;                     // VBI decoders
    ULONG  Country;                     // VBI decoders
} 

KSPROPERTY_TUNER_FREQUENCY_S, *PKSPROPERTY_TUNER_FREQUENCY_S;

This code shows the interface and some data structures. Maybe you will not believe this, but the most difficult part of the whole C-to-C# translation was to get a usable data structure.

Now the Real Stuff: Go from C to C#

QueryInterface() is, in C#, a typecast of the IAMTVTuner object to the IKsPropertySet interface pointer. QuerySupported() is called to check if the property data can be read and written. In the C# version, the TUNER_MODE_CAPS_S property data structure will not be used, mainly because this keeps the code simple. Instead of this, the minimum and maximum frequency are set to fixed values. It is up to you how to initialize. For NTSC countries, the range is normally 45 till 801MHz and for PAL countries, the range is normally 45 till 863MHz. Keep in mind that TV tuners might have a different range.

The TUNER_FREQUENCY_S property data is read first, so it can be used for writing too. The main reason is that this way, all attributes will be initialized. The tuning flag in the TUNER_FREQUENCY_S property data attribute TuningFlags will be set to KS_TUNER_TUNING_EXACT because this is what I want to do: change the tuning frequency into the specified value. The new frequency is stored in the TUNER_FREQUENCY_S property data attribute Frequency.

After this, the TUNER_FREQUENCY_S property data is written with the set method. To make the code more correct, I added a check for using get and set via QuerySupported. There is little chance that the functionality is not supported. Putting everything together, the following solution came up:

/// <summary />
/// Set broadcast TV tuning frequency using the IKsPropertySet interface.
/// </summary />

public int SetFrequency(int Freq)
{ 
    int hr;
    IKsPropertySet pKs = tvTuner as IKsPropertySet;
    KSPropertySupport dwSupported = new KSPropertySupport();
    DshowError errorCode = DshowError.VFW_NO_ERROR;
        
    // Use IKsPropertySet interface (interface for Vfw like property
    // window) for getting/setting tuner specific information.
    // Check first if the Property is supported.
    if(pKs == null)
    {
        errorCode = DshowError.VFW_E_NO_INTERFACE;
        return (int)errorCode;
    }
    // Use IKsPropertySet interface (interface for Vfw like property
    // window) for getting and setting tuner specific information
    // like the real broadcast frequency.
    hr = pKs.QuerySupported(
        PROPSETID_TUNER, 
        (int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
        out dwSupported);
    if(hr == 0)
    {
        if( ((dwSupported & KSPropertySupport.Get)== 
            KSPropertySupport.Get)&&
            ((dwSupported & KSPropertySupport.Set)== KSPropertySupport.Set)&
            (Freq >= this.minFrequency && Freq <= 
            this.maxFrequency) )
        {
            // Create and prepare data structures
            KSPROPERTY_TUNER_FREQUENCY_S Frequency = 
                new KSPROPERTY_TUNER_FREQUENCY_S();
            IntPtr freqData = Marshal.AllocCoTaskMem(
                Marshal.SizeOf(Frequency));
            IntPtr instData = Marshal.AllocCoTaskMem(
                Marshal.SizeOf(Frequency.Instance));
            int cbBytes = 0;

            // Convert the data
            Marshal.StructureToPtr(Frequency, freqData, true);
            Marshal.StructureToPtr(Frequency.Instance, instData, true);

            hr = pKs.Get(
                PROPSETID_TUNER,
                (int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
                instData,
                Marshal.SizeOf(Frequency.Instance),
                freqData,
                Marshal.SizeOf(Frequency),
                out cbBytes);
            if(hr == 0)
            {
                // Specify the TV broadcast frequency and tuning flag
                Frequency.Instance.Frequency = Freq;
                Frequency.Instance.TuningFlags =
                    (int)KS_TUNER_TUNING_FLAGS.TUNING_EXACT;

                // Convert the data
                Marshal.StructureToPtr(Frequency, freqData, true);
                Marshal.StructureToPtr(Frequency.Instance, instData, true);

                // Now change the broadcast frequency
                hr = pKs.Set(
                    PROPSETID_TUNER,
                    (int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
                    instData,
                    Marshal.SizeOf(Frequency.Instance),
                    freqData,
                    Marshal.SizeOf(Frequency));
                if(hr < 0)
                {
                    errorCode = (DshowError)hr;
                }
            } 
            else
            {
                errorCode = (DshowError)hr;
            }
            if(freqData != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(freqData);
            }
            if(instData != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(instData);
            }
        }
    } 
    else
    {   // QuerySupported
        errorCode = (DshowError)hr;
    }

    return (int)errorCode;
}

Points of Interest

The most difficult part for me was to make a data structure that did not cause an error when using the get and set methods. The get and set methods need parameters pointing to the tuner data. In C, the get/set interface needs a pointer (done via the two macros mentioned before) to the whole data structure KSPROPERTY_TUNER_FREQUENCY_S, the part with only the tuner-specific attributes (starting from attribute Frequency) and the size of the data. C# does not know pointers, so I needed a different solution.

The first step was to translate the KSPROPERTY_TUNER_FREQUENCY_S structure into C# using [StructLayout( .... How to access the tuner-specific attributes in a C# friendly way? Well, I decided to put the tuner-specific attributes in a new TUNER_FREQUENCY-specific data structure and create another structure with the KSPROPERTY structure and TUNER_FREQUENCY. Still, the code did not work as expected. For some unknown reason, I decided to change the size of the data structure by adding dummy attributes. Now the C# code came to life.

/// <summary />
/// KSPROPERTY tuner frequency data structure
/// </summary />
[StructLayout(LayoutKind.Sequential)]
public struct KSPROPERTY_TUNERFREQUENCY
{
    /// <summary /> Hz </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  Frequency;                
    /// <summary /> Hz (last known good) </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  LastFrequency;          
    /// <summary /> KS_TUNER_TUNING_FLAGS </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  TuningFlags;            
    /// <summary /> DSS </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  VideoSubChannel;        
    /// <summary /> DSS </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  AudioSubChannel;
    /// <summary /> Channel number </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  Channel;                
    /// <summary /> Country number </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  Country;                
    /// <summary /> Undocumented or error ... </summary />
    [MarshalAs(UnmanagedType.U4)]
    public int  Dummy;                  
    // Dummy added to get a successful return of the Get, Set 
    // function
}

/// <summary />
/// KSPROPERTY tuner frequency structure including the tuner 
/// frequency data structure.
/// Size is 6 + 7 (+ 1 dummy) ints
/// </summary />
[StructLayout(LayoutKind.Sequential)]
public struct KSPROPERTY_TUNER_FREQUENCY_S
{
    /// <summary /> Property Guid </summary />
    public KSPROPERTY Property;            
    /// <summary /> Tuner frequency data structure 
    /// </summary />
    public KSPROPERTY_TUNERFREQUENCY Instance;    
}
// KSPROPERTY_TUNER_FREQUENCY_S, *PKSPROPERTY_TUNER_FREQUENCY_S;

The code in this example is a follow-up of the code I used in two of my previous articles, Audio File Saving for the DirectX.Capture Class and Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library. This example has the following enhancements:

  • Added a TV tuning frequency up/down box to test the new TV fine-tuning functionality. The TV fine-tune functionality is put in a new class TVFineTune which inherits from the original Tuner class. To use the TV tuner's new (and old) functionality in the code, the TVFineTune class should be used instead of the Tuner class.
  • A new class called TVSelections was added, so the TV channel selection functionality becomes far more usable than the one in the original implementation. The implementation is very simple and it can be modified easily. The channel selections are hard-coded! To use it on your own system, the settings needs to be modified. The code shows three tables: one with the channel names, one with the tuning frequency and one with the channel number. The use of channel numbers is very system-specific; I added the values to show that it cannot be used that easily unless you know which frequency corresponds with it. Of course, it is possible to write a nice program using Channel to set the TV Tuner and GetVideoFrequnecy() to get the corresponding tuning frequency. Why do this, though, if you know the TV tuning frequency already?
  • Support for TV card drivers that support a video device only. There is no separate audio device to choose, so the audio device needs to be found a little bit differently. The modifications were needed so my Hauppauge PVR150 MCE TV card could be used with the newest TV card driver in the code example.
  • This code example has a possible solution to get TV sound. This solution was needed because there were problems with getting audible sound and the selection of audio sources. The problems also show up in the original version of the DirectX.Capture Class Library written by Brian Low. So it is not a new problem introduced by my code enhancements. Via debugging, I noticed that in some cases the audio source and/or video source becomes invalid. As a result of this, exceptions are fired. For that reason, the data causing the problem is reinitialized upon using it in PropertyPages, VideoSources and/or AudioSources.
  • The code example has been tested with Visual Studio 2003 as well as Visual Studio 2005. Conflicts between these two compiler versions might occur. I added the conditional VS2003 for now to handle one of the conflicts I encountered when using the form signal for closing the main form. In Visual Studio 2003 this signal has the name Closed, while in Visual Studio 2005 this signal has the name FormClosed. In this code example, I removed the line of code that specifies the function needing to be called when closing the main form (but okay, that line should be put back). But there is more, the form specific code is not the same for Visual Studio 2003 and Visual Studio 2005, the code is generated by Visual Studio. It is not my intention to solve these types of conflicts.
  • The DirectX.Capture examples that go with this article contain new solutions to increase stability. Still, exceptions may occur, but most of them can be solved by either redesigning this code example or by catching and handling the exceptions in a more appropriate way. Keep in mind that this code example is for learning purposes only. It teaches you how to use DirectShow in C# and teaches you to use GUI. Exceptions that occur should not be seen as a problem, but as a challenge! The major advantage of an exception is that it tells you when something goes wrong. As a side effect, the program fails and, by debugging, the cause of the problem can be found much easier because you know where to start.

Important: Choose DirectShowLib or DShowNET

The DirectX.Capture class example uses DShowNET. The DirectX.Capture Class Library (Refresh) uses DirectShowLib, which is more complete than DShowNET. It is up to you what to use.

Extra Code Example with New Interesting Features

SampleGrabber sample image

I got a lot of questions on specific features such as using SampleGrabber and video preview quality. I made a new code example for those who want to play with these features. As an extra feature, this example supports FM Radio also.

Using FM Radio

I added this feature because it might be useful when testing code for TV tuners that support FM Radio. FM Radio can be selected only if the TV tuner supports this. I did not add a broadcast station selection list (yet). For testing reasons, it was sufficient for me to switch between the TV-specific code and the FM Radio-specific code.

#if DSHOWNET
private DShowNET.AMTunerModeType TunerModeType
#else
private AMTunerModeType TunerModeType
#endif
{
    get { return this.tunerModeType; }
    set
    {
        this.tunerModeType = value;
        if((this.capture != null)&&(this.capture.Tuner != null))
        {
            this.capture.Tuner.AudioMode = value;
            this.capture.Tuner.InputType = this.tunerInputType;
            this.capture.Tuner.TuningSpace = 31;
        }
            
#if DSHOWNET
        if(value == DShowNET.AMTunerModeType.TV)
#else
        if (value == AMTunerModeType.TV)
#endif
        {
            if((this.capture != null)&&(this.capture.Tuner != null))
            {
                this.capture.Tuner.Channel = 
                    this.tvSelections.GetChannelNumber;
                this.capture.Tuner.SetFrequency(this.LastTvFrequency);
            }
            this.numericUpDown1.Maximum = this.capture.Tuner.MaxFrequency;
            this.numericUpDown1.Minimum = capture.Tuner.MinFrequency;
            this.numericUpDown1.Increment = 500000;
            this.numericUpDown1.Value = this.LastTvFrequency;
            this.numericUpDown1.Enabled = true;
        }
        else
#if DSHOWNET
            if(value == DShowNET.AMTunerModeType.FMRadio)
#else
           if (value == AMTunerModeType.FMRadio)
#endif
        {
            if((this.capture != null)&&(this.capture.Tuner != null))
            {
                this.capture.Tuner.Channel = this.LastFMRadioFrequency;
            }
            this.numericUpDown1.Minimum = this.capture.Tuner.ChanelMinMax[0];
            this.numericUpDown1.Maximum = this.capture.Tuner.ChanelMinMax[1];
            this.numericUpDown1.Increment = 50000;
            this.numericUpDown1.Value = this.LastFMRadioFrequency;
            this.numericUpDown1.Enabled = true;
        }
    }
}

Video De-interlacing

Quite often, the video preview quality looks bad. Using a de-interlace filter may improve the preview quality dramatically. The main reason I added the de-interlace filter had nothing to do with the preview quality. I added this filter because it enabled a preview for my Hauppauge PVR150 TV-card. Usually I got a black screen, so no video was displayed. With this filter in, however, video seems to be played right away.

I chose the Alpary filter because it can be used freely. Other filters might be usable, too, but I did not test them. The function FindDeinterlaceFilter() scans filters.LegacyFilters to find the specified filter. It is easy to specify a different filter.

string filterName = "Alparysoft Deinterlace Filter";
Filter DeInterlace = null;

for (int i = 0; i < this.filters.LegacyFilters.Count; i++)
{
    if (filters.LegacyFilters[i].Name.StartsWith(filterName))
    {
        this.capture.DeInterlace = filters.LegacyFilters[i];
        return true;
    }
}

This filter is added to the graph just before calling RenderStream() to render the video. If the filter is in, RenderStream() will usually add this filter automatically. In some cases, the de-interlace filter will not be added to the graph. In those cases, extra code is needed to add the de-interlace filter explicitly. The preview quality will be better when VMR9 (Video Mixing Renderer 9) is used, even if no extra de-interlace filter is used. Interestingly, VMR9 offers de-interlacing itself (IVMRDeinterlaceControl9). It can be used via the VMR9 property page or via software control.

Video De-interlacing and VMR9

I added an option to the program to use VMR9 more easily. To initialize the proper video renderer, the function InitVideoRenderer should be called upon rendering video.

#if DSHOWNET
/// <summary />
/// CLSID_VideoRenderer
/// </summary />
[ComImport, Guid("70e102b0-5556-11ce-97c0-00aa0055595a")]
public class VideoRenderer
{
}
#endif

/// <summary />
/// Use VMR9 flag, if false use the video renderer instead
/// </summary />
private bool useVMR9 = false;

private IBaseFilter videoRendererFilter = null;
/// <summary />
/// Check if VMR9 should be used
/// </summary />
public bool UseVMR9
{
    get { return this.useVMR9; }
    set    { this.useVMR9 = value; }
}

private bool InitVideoRenderer()
{
    if(this.useVMR9)
    {
        this.videoRendererFilter = (IBaseFilter)new VideoMixingRenderer9();
    }
    else
    {
        this.videoRendererFilter = (IBaseFilter)new VideoRenderer();
    }

    if(this.videoRendererFilter != null)
    {
        this.graphBuilder.AddFilter(this.videoRendererFilter, 
            "Video Renderer");
    }
    return false;
}

The video renderer is put in the graph via RenderStream:

#if DSHOWNET
hr = captureGraphBuilder.RenderStream(ref cat, ref med, videoDeviceFilter, 
    null, this.videoRendererFilter);
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat), 
    DsGuid.FromGuid(med), videoDeviceFilter, null, this.videoRendererFilter);
#endif

Grabbing a Frame

DirectShow offers two basic methods to grab a frame or an image that is going to be rendered. The first method to grab a frame is the SampleGrabber method. Via SampleGrabber, a frame can be grabbed via a frame event or via GetCurrentBuffer. The second method involves use of VideoMixingRenderer or the BasicVideo interface by calling GetCurrentImage(). This method can be used with VMR or VMR9 for sure, and sometimes this might work for Video Renderer also.

The SampleGrabber code cannot be used if the capture device has a VP (Video Port) pin. Only video cards with a video capture device, such as Nivdia MX460 Vivo video card, will have such a pin. This is not a big concern because either SampleGrabber can be used via the capture pin or GetCurrentImage() can be used via the VMR that is connected with the VP pin to render the video.

In this example, the SampleGrabber method is used to grab a frame via a frame event. This method is used in most examples that are floating around on the Internet. Another advantage is that you have the choice to capture one frame or all frames. Most examples do not show what actions are really needed to get SampleGrabber working. This example shows what needs to be done.

First, I will give a description of the code changes that should be put in DirectX.Capture\Capture.cs. The function InitSampleGrabber adds the SampleGrabber filter to the graph and this function also initializes the media type it should be used for. This function should be called upon rendering Video for preview.

private bool InitSampleGrabber()
{
    // Get SampleGrabber
    this.sampGrabber = new SampleGrabber() as ISampleGrabber;

    if(this.sampGrabber == null)
    {
        return false;
    }

#if DSHOWNET
    this.baseGrabFlt    = (IBaseFilter)this.sampGrabber;
#else
    this.baseGrabFlt = sampGrabber as IBaseFilter;
#endif

    if(this.baseGrabFlt == null)
    {
        Marshal.ReleaseComObject(this.sampGrabber);
        this.sampGrabber = null;
    }
    AMMediaType media = new AMMediaType();

    media.majorType    = MediaType.Video;
    media.subType    = MediaSubType.RGB24;
    media.formatPtr = IntPtr.Zero;
    hr = sampGrabber.SetMediaType(media);
    if(hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    hr = graphBuilder.AddFilter(baseGrabFlt, "SampleGrabber");
    if(hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    hr = sampGrabber.SetBufferSamples(false);
    if( hr == 0 )
    {
        hr = sampGrabber.SetOneShot(false);
    }
    if( hr == 0 )
    {
        hr = sampGrabber.SetCallback(null, 0);
    }
    if( hr < 0 )
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    return true;
}

It might be possible that the selected media type RGB24 is not usable. In such a case, modify the code. The following line of code shows how to get SampleGrabber in the graph upon rendering video:

#if DSHOWNET
hr = captureGraphBuilder.RenderStream(ref cat, ref med, videoDeviceFilter, 
    this.baseGrabFlt, this.videoRendererFilter);
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat), 
    DsGuid.FromGuid(med), videoDeviceFilter, this.baseGrabFlt, 
    this.videoRendererFilter); 
#endif

If GetCurrentBuffer would be used, then SetBufferSamples(true) should be called instead of SetBufferSamples(false). The function SetMediaSampleGrabber retrieves media-specific data and stores that data for later use. This function should be called upon initializing the preview window.

private int snapShotWidth = 0;
private int snapShotHeight = 0;
private int snapShotImageSize = 0;
private bool snapShotValid = false;

private void SetMediaSampleGrabber()
{
    this.snapShotValid = false;
    if((this.baseGrabFlt != null)&&(this.AllowSampleGrabber))
    {
        AMMediaType media = new AMMediaType();
        VideoInfoHeader videoInfoHeader;
        int hr;

        hr = sampGrabber.GetConnectedMediaType(media);
        if (hr < 0)
        {
            Marshal.ThrowExceptionForHR(hr);
        }
       
        if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr ==
            IntPtr.Zero))
        {
            hrow new NotSupportedException(
                "Unknown Grabber Media Format");
        }

        videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(
            media.formatPtr, typeof(VideoInfoHeader));
        this.snapShotWidth = videoInfoHeader.BmiHeader.Width;
        this.snapShotHeight = videoInfoHeader.BmiHeader.Height;
        this.snapShotImageSize = videoInfoHeader.BmiHeader.ImageSize;
        Marshal.FreeCoTaskMem(media.formatPtr);
        media.formatPtr = IntPtr.Zero;
        this.snapShotValid = true;
    }

    if (!this.snapShotValid)
    {
        this.snapShotWidth = 0;
        this.snapShotHeight = 0;
        this.snapShotImageSize = 0;
    }
}

Keep in mind that if the media type changes, the number of bytes per pixel (stride) might change as well. The code for grabbing the frame might look like this:

/// <summary /> Interface frame event </summary />
public delegate void HeFrame(System.Drawing.Bitmap BM);
/// <summary /> Frame event </summary />
public event HeFrame FrameEvent2;
private    byte[] savedArray;
private    int    bufferedSize;

int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, 
    int BufferLen )
{
    this.bufferedSize = BufferLen;
        
    int stride = this.SnapShotWidth * 3;

    Marshal.Copy( pBuffer, this.savedArray, 0, BufferLen );

    GCHandle handle = GCHandle.Alloc( this.savedArray, GCHandleType.Pinned );
    int scan0 = (int) handle.AddrOfPinnedObject();
    scan0 += (this.SnapShotHeight - 1) * stride;
    Bitmap b = new Bitmap(this.SnapShotWidth, this.SnapShotHeight, -stride, 
        System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr) scan0 );
    handle.Free();
    SetBitmap=b;
    return 0;
}
/// <summary /> capture event, triggered by buffer callback. </summary />
private void OnCaptureDone()
{
    Trace.WriteLine( "!!DLG: OnCaptureDone" );
}
/// <summary /> Allocate memory space and set SetCallBack </summary />
public void GrapImg()
{
    Trace.Write ("IMG");
    if( this.savedArray == null )
    {
        int size = this.snapShotImageSize;
        if( (size < 1000) || (size > 16000000) )
            return;
        this.savedArray = new byte[ size + 64000 ];
    }
    sampGrabber.SetCallback( this, 1 );
}
/// <summary /> Transfer bitmap upon firing event </summary />
public System.Drawing.Bitmap SetBitmap
{
    set
    {
        this.FrameEvent2(value);
    }
}

Most examples I saw did not release SampleGrabber-specific data. This code example should do that job properly... but errors might still occur. To get the SampleGrabber code working, the main program in CaptureTest\CaptureTest.cs must be modified too. First, the CaptureTest form needs to get two extra buttons and a PictureBox. In the code example, I added special code to add a small PictureBox. You can resize the form as well as resize and reposition the buttons and PictureBox yourself.

I made a very small PictureBox on purpose. I added also some code to hide the video file saving buttons and filename when SampleGrabber is put in the graph. I did this on purpose, as it gave me some free design space on the CaptureTest form. This also prevents a possible interaction with the video file saving functionality.

private void button1_Click(object sender, System.EventArgs e)
{
    this.capture.FrameEvent2 += new Capture.HeFrame(this.CaptureDone);
    this.capture.GrapImg();
}

private void CaptureDone(System.Drawing.Bitmap e)
{
    this.pictureBox1.Image=e;
    // Show only the selected frame ...
    // If you want to capture all frames, then remove the next line
    this.capture.FrameEvent2 -= new Capture.HeFrame(this.CaptureDone); 
}

private void button2_Click(object sender, System.EventArgs e)
{
    if( (this.pictureBox1 != null)&&
        (this.pictureBox1.Image != null)&&
        (this.imageFileName.Text.Length > 0) )
    {
        this.pictureBox1.Image.Save(this.imageFileName.Text, 
            System.Drawing.Imaging.ImageFormat.Bmp);
    }
}

Features are Made Optional

In the real code example, I added the new features as options. To use a new feature, the corresponding option needs to be selected first. The main reason for doing this is that a program sometimes failed at first use, due to one of the option settings. Now you can just change the option value and try again. There is one demand: a new value of an option becomes active upon (re)selecting the Audio or Video device. To get the options properly initialized, the function InitMenu() is added. This function should be called when a capture device is (re)selected.

private void initMenu()
{
    if (this.capture != null)
    {
        // Set flag only if capture device is initialized
        this.capture.AllowSampleGrabber = 
            this.menuAllowSampleGrabber1.Checked;
        this.menuSampleGrabber1.Enabled = 
            this.menuAllowSampleGrabber1.Checked;
        this.menuSampleGrabber1.Visible = 
            this.menuAllowSampleGrabber1.Checked;
        this.capture.VideoSource = this.capture.VideoSource;
        this.capture.UseVMR9 = this.menuUseVMR9.Checked;
        this.menuUseDeInterlace1.Checked = this.FindDeinterlaceFilter(
            this.menuUseDeInterlace1.Checked);
    }
}

History

  • January 31, 2007: First release
  • August 1, 2007: Added support for FM Radio and video de-interlacing. The solution of capturing audio via the video device filter has been improved. Furthermore, the code example supports either DShowNET or DirectShowLib as interface library via the conditional DSHOWNET.
  • August 10, 2007: Added SampleGrabber and VMR9 support in an extra code example
  • November 28, 2007: Fixed minor bugs in downloads
  • February 17, 2008: Minor text modifications, links corrected and the SampleGrabber example has been modified to support Visual Studio 2003 and Visual Studio 2005. Please use either one of these versions only. When both Visual Studio versions are used, put the code in different directories!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

almere109



Occupation: Web Developer
Location: Netherlands Netherlands

Other popular Audio and Video articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 74 (Total in Forum: 74) (Refresh)FirstPrevNext
Subject  Author Date 
QuestionPerformance of DemomemberFethi Altunyuva0:06 25 Jun '08  
AnswerRe: Performance of Demomemberalmere1099:08 25 Jun '08  
AnswerRe: Performance of Demomemberalmere10910:03 25 Jun '08  
GeneralRe: Performance of DemomemberFethi Altunyuva4:58 26 Jun '08  
GeneralRe: Performance of Demomemberalmere10911:25 26 Jun '08  
GeneralAudio troublesmemberArmanisoft7:47 29 May '08  
GeneralRe: Audio troublesmemberalmere1099:40 29 May '08  
GeneralAudio Problems while PreviewingmemberMember 88950119:15 10 Apr '08  
GeneralRe: Audio Problems while Previewingmemberalmere10922:23 10 Apr '08