|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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 BackgroundThe 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 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 Using the CodeTo 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);
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 hr = m_pKSProp->QuerySupported(PROPSETID_TUNER, KSPROPERTY_TUNER_MODE_CAPS,
&dwSupported);
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;
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 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#
The After this, the /// <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 InterestThe most difficult part for me was to make a data structure that did not cause an error when using the The first step was to translate the /// <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:
Important: Choose DirectShowLib or DShowNETThe DirectX.Capture class example uses Extra Code Example with New Interesting Features
I got a lot of questions on specific features such as using Using FM RadioI 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-interlacingQuite 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 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 Video De-interlacing and VMR9I added an option to the program to use VMR9 more easily. To initialize the proper video renderer, the function #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 #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 FrameDirectShow 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 The In this example, the First, I will give a description of the code changes that should be put in DirectX.Capture\Capture.cs. The function 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 #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 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 I made a very small 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 OptionalIn 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 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
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||