DirectShow: 使用 C# 中的 IKsPropertySet 进行电视微调
DirectX.Capture 类在电视微调中使用 IKsPropertySet 的增强功能。
引言
本文是我之前文章 Audio File Saving for the DirectX.Capture Class 和 Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library 的续篇。那些文章描述了如何为捕获的音频和视频进行文件保存。本文将展示一种解决方案,通过选择特定的频道名称或仅输入广播频率来获取特定的广播电台,以捕获音频和视频。与本文相关的,还描述了一些额外的功能,如 VMR9、去隔行和 FM 收音机。
背景
给我基本想法的文章是 Liaqat Fayyaz 撰写的 DirectShow - Fine TV Tuning using IKsPropertySet。
当我开始这个项目时,我想,“这里发生了什么?” 我一步一步地发现了发生了什么,并发现将大约 70 行 C 代码翻译成可工作的 C# 代码并不容易!我必须理解很多 DirectShow 特定代码的含义。找到有用的信息非常困难。Liaqat Fayyaz 的文章帮助我找到了正确的术语,然后,经过深思熟虑,我发现了如何在 C# 中使用非托管数据而不会出错。最后,我编写了 300 行 C# 代码来实现电视微调。300 行的代码是因为还需要一些“看不见的 C 代码”,这些代码位于 DirectX SDK 的一些包含文件中。为了理解这一切,我不得不多次访问 MSDN。
选择电视频道或电视频道通常是通过选择 1 到 368 之间的频道号(针对荷兰、欧洲)。这些频道号基于预定义的频率表,频率在 45 到 863 MHz 之间。确切的频率取决于国家、视频标准(PAL、NTSC、SECAM)和电视调谐器。通过 IAMTuner
接口和 put_Channel
方法,可以选择所需的电视频道。
然而,并非所有电视频道都能调谐,这是一个主要问题。这是我更喜欢通过仅选择有线电视提供商提供的频率之一来解决问题的主要原因。只需输入频率,进行一些微调,然后观看电视节目。幸运的是,电视调谐器对象的 IKsPropertySet
接口提供了此解决方案。
Using the Code
要理解 C# 代码,您必须理解我用作起点的原始 C 代码。此代码可以在 Liaqat Fayyaz 的文章 DirectShow - Fine TV Tuning using IKsPropertySet 中找到。基于这篇文章,我编写了一个 C# 解决方案。我们将从一些原始 C 代码和简短的解释开始。查看下一个宏,您可能会想,“哇,这是怎么回事?”嗯,至少我是这么想的。
#define INSTANCEDATA_OF_PROPERTY_PTR(x) ((PKSPROPERTY((x))) + 1)
这是用于输入和/或作为输出返回的实际数据的指针。
#define INSTANCEDATA_OF_PROPERTY_SIZE(x) (sizeof((x)) - sizeof(KSPROPERTY))
这是用于输入和输出的实际属性数据结构的大小。
hr = m_pTvtuner->QueryInterface(IID_IKsPropertySet, (void**)&m_pKSProp);
QueryInterface
查询对象的接口指针,在此情况下是电视调谐器对象。如果没有这样的接口,则无事可做。
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));
这显示了在 SetFrequency()
函数中使用的 TUNER_MODE_CAPS_S
和 TUNER_FREQUENCY_S
属性的数据结构和内存分配。TUNER_MODE_CAPS_S
属性数据结构提供了电视调谐器设备的功能。所需的重要信息包括可调谐的最小频率和最大频率。TUNER_FREQUENCY_S
属性数据结构用于设置频率。
hr = m_pKSProp->QuerySupported(PROPSETID_TUNER, KSPROPERTY_TUNER_MODE_CAPS,
&dwSupported);
QuerySupported
检查是否可以使用特定属性的 get
或 set
方法。
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
属性数据用于初始化调谐标志。新频率被复制到 TUNER_FREQUECY_S
属性数据中。
Frequency.Frequency=Freq;
if(ModeCaps.Strategy==KS_TUNER_STRATEGY_DRIVER_TUNES)
Frequency.TuningFlags=KS_TUNER_TUNING_FINE;
else
Frequency.TuningFlags=KS_TUNER_TUNING_EXACT;
新频率得到验证。如果频率在范围内,频率更改将通过 set
方法发送到电视调谐器对象。
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;
如果一切顺利,新的电视频率将改变,电视频道就会出现。如果之前的代码不完整,那么也必须考虑以下代码!所示代码来自 DirectX SDK 中的 *strmif.h*、*ks.h* 和 *ksmedia.h*。
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;
这段代码展示了接口和一些数据结构。您可能不会相信,但从 C 到 C# 翻译过程中最困难的部分是获得一个可用的数据结构。
现在是正题:从 C 到 C#
在 C# 中,QueryInterface()
是将 IAMTVTuner
对象转换为 IKsPropertySet
接口指针。调用 QuerySupported()
来检查属性数据是否可以读写。在 C# 版本中,TUNER_MODE_CAPS_S
属性数据结构将不会被使用,主要是为了保持代码的简洁。取而代之的是,最小和最大频率被设置为固定值。如何初始化取决于您。对于 NTSC 国家,范围通常是 45 到 801MHz,对于 PAL 国家,范围通常是 45 到 863MHz。请记住,电视调谐器可能有不同的范围。
首先读取 TUNER_FREQUENCY_S
属性数据,以便也可以用于写入。主要原因是这样可以初始化所有属性。TUNER_FREQUENCY_S
属性数据属性 TuningFlags
中的调谐标志将被设置为 KS_TUNER_TUNING_EXACT
,因为我想要做的就是:将调谐频率更改为指定值。新频率存储在 TUNER_FREQUENCY_S
属性数据属性 Frequency
中。
之后,使用 set
方法写入 TUNER_FREQUENCY_S
属性数据。为了使代码更正确,我添加了使用 QuerySupported
进行 get
和 set
的检查。功能不支持的可能性很小。将所有内容结合起来,得到了以下解决方案。
/// <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;
}
KSPROPERTY_TUNER_FREQUENCY
对我来说,最困难的部分是创建一个在使用 get
和 set
方法时不会出错的数据结构。get
和 set
方法需要指向调谐器数据的参数。在 C 中,get
/set
接口需要一个指向整个数据结构 KSPROPERTY_TUNER_FREQUENCY_S
的指针(通过前面提到的两个宏完成),指向仅包含调谐器特定属性的部分(从属性 Frequency
开始),以及数据的大小。C# 不支持指针,所以我需要一个不同的解决方案。
第一步是使用 [StructLayout( ...
将 KSPROPERTY_TUNER_FREQUENCY_S
结构翻译成 C#。我们如何以 C# 友好的方式访问调谐器特定属性?好吧,我决定将调谐器特定属性放在一个新的 TUNER_FREQUENCY
特定数据结构中,并创建另一个包含 KSPROPERTY
结构和 TUNER_FREQUENCY
的结构。但代码仍未按预期工作。出于未知原因,我决定通过添加虚拟属性来更改数据结构的大小。现在,C# 代码活过来了。
/// <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;
此代码示例中的附加功能
视频去隔行
视频预览质量常常看起来很糟糕。使用去隔行过滤器可以显著提高预览质量。我添加去隔行过滤器的主要原因与预览质量无关。我添加这个过滤器是因为它支持我的 Hauppauge PVR150 电视卡预览。通常,我看到的是黑屏,所以没有显示视频。但是,有了这个过滤器,视频似乎就能立即播放了。
我选择了 Alpary 过滤器,因为它免费使用。其他过滤器,例如 Sourceforge.net 上的 ffdshow 过滤器,也可能可用。但我没有测试过任何。FindDeinterlaceFilter()
函数会扫描 filters.LegacyFilters
来查找指定的过滤器。指定不同的过滤器很容易。
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;
}
}
在调用 RenderStream()
来渲染视频之前,将此过滤器添加到图形中。如果过滤器在,RenderStream()
通常会自动添加此过滤器。在某些情况下,不会将去隔行过滤器添加到图中。在这种情况下,需要额外的代码来显式添加去隔行过滤器。使用 VMR9(视频混合渲染器 9)可以提高预览质量,即使没有额外的去隔行过滤器。有趣的是,VMR9 本身就提供去隔行功能(IVMRDeinterlaceControl9
)。它可以通过 VMR9 属性页或通过软件控制来使用。
视频去隔行和 VMR9
我向程序添加了一个选项,以便更轻松地使用 VMR9。为了初始化适当的视频渲染器,在渲染视频时应调用 InitVideoRenderer
函数。
#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;
}
视频渲染器通过 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
使用 FM 收音机
我添加此功能是因为当测试支持 FM 收音机的电视调谐器代码时,它可能很有用。只有当电视调谐器支持 FM 收音机时才能选择它。我还没有添加广播电台选择列表。出于测试原因,我只需在电视特定代码和 FM 收音机特定代码之间切换就足够了。
电视和 FM 收音机之间的切换看起来很简单,但实际上并非如此。当访问调谐器属性页时,新设置可能需要被考虑。FM 收音机不需要视频预览,我们该如何处理?目前,视频预览被忽略了。FM 收音机需要不同的预设;目前此功能尚不支持。
#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 = this.DefaultTuningSpace;
this.capture.Tuner.CountryCode = this.DefaultCountryCode;
}
this.numericUpDown1.Enabled = false;
switch(value)
{
case AMTunerModeType.TV:
if((this.capture != null)&&(this.capture.Tuner != null))
{
this.numericUpDown1.Maximum = this.capture.Tuner.MaxFrequency;
this.numericUpDown1.Minimum = this.capture.Tuner.MinFrequency;
this.numericUpDown1.Value = this.LastTvFrequency;
this.numericUpDown1.Increment = 500000;
if(this.LastTvFrequency == 0)
{
this.LastTvFrequency = this.tvSelections.GetChannelFrequency;
}
this.capture.Tuner.SetFrequency(this.LastTvFrequency);
this.numericUpDown1.Enabled = true;
}
break;
case AMTunerModeType.FMRadio:
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;
}
break;
default:
break;
}
}
}
功能可选
在实际代码示例中,我将新功能添加为选项。要使用一项新功能,首先需要选择相应的选项。这样做的主要原因是一些程序在首次使用时会因某个选项设置而失败。然后,您可以更改选项值并重试。有一个要求:选项的新值将在(重新)选择音频或视频设备后生效。为了正确初始化选项,添加了 InitMenu()
函数。在(重新)选择捕获设备时应调用此函数。
private void initMenu()
{
if (this.capture != null)
{
this.capture.VideoSource = this.capture.VideoSource;
this.capture.UseVMR9 = this.menuUseVMR9.Checked;
this.menuUseDeInterlace1.Checked =
this.FindDeinterlaceFilter(this.menuUseDeInterlace1.Checked);
}
}
关注点
与之前的文章 Audio File Saving for the DirectX.Capture Class 和 Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library 相比,大部分功能都得以保留,并移除了一些功能,使其成为一个更易用的电视程序。
- 添加了一个电视调谐频率上/下框来测试新的电视微调功能。电视微调功能被放入了一个名为
TVFineTune
的新类中,该类继承自原始的Tuner
类。要在代码中使用电视调谐器的新(旧)功能,应使用TVFineTune
类而不是Tuner
类。 - 添加了一个名为
TVSelections
的新类,使电视选台功能比原始实现更易用。实现非常简单,可以轻松修改。频道选择是硬编码的!要在您自己的系统上使用它,需要修改设置。代码显示了三个表:一个包含频道名称,一个包含调谐频率,一个包含频道号。频道号的使用非常特定于系统;我添加了这些值以表明,除非您知道哪个频率与之对应,否则它不能轻易使用。当然,可以编写一个不错的程序,使用Channel
来设置电视调谐器,使用GetVideoFrequnecy()
来获取相应的调谐频率。但既然您已经知道电视调谐频率,为什么要这样做呢? - 添加了初始化与国家/地区相关设置的功能。基于与国家/地区相关设置,此代码尝试检索电视调谐的最小和最大频率。
- 支持颜色空间和视频标准的更改。
- 支持仅支持视频设备的电视卡驱动程序。没有单独的音频设备可供选择,因此需要以略有不同的方式找到音频设备。修改是必要的,以便我的 Hauppauge PVR150 MCE 电视卡可以在代码示例中使用最新的电视卡驱动程序。
- 此代码示例提供了一个获取电视声音的可能解决方案。这是因为在获取可听声音和选择音频源时存在问题。这些问题也出现在 Brian Low 编写的 DirectX.Capture Class Library 的原始版本中。所以,这不是由我的代码增强引入的新问题。通过调试,我注意到在某些情况下,音频源和/或视频源会变得无效。因此,会引发异常。出于这个原因,在 PropertyPages、VideoSources 和/或 AudioSources 中使用导致问题的数据时,会对其进行重新初始化。
- 此代码示例已在 Visual Studio 2003 和 Visual Studio 2005 中进行了测试。这两个编译器版本之间可能会发生冲突。我添加了条件
VS2003
来显示代码差异。一个区别是,在 Visual Studio 2003 中,一个信号名为Closed
,而在 Visual Studio 2005 中,这个信号名为FormClosed
。此代码示例有两个版本:使用 DShowNET 作为 DirectShow 接口库的 Visual Studio 2003 版本,以及使用 DirectShowLib-2005 作为 DirectShow 接口库的 Visual Studio 2005 版本。仍然可以使用 DShowNET 配合 Visual Studio 2005,但我没有测试过。如果需要两个 Visual Studio 版本,请为此代码示例使用不同的目录,以防止生成问题。我的目的不是解决可能出现的几个 Visual Studio 版本之间的编码冲突和生成问题。 - 重要提示:选择 DirectShowLib 或 DShowNET!DirectX.Capture 类示例使用 DShowNET。 DirectX.Capture Class Library (Refresh) 使用 DirectShowLib,它比 DShowNET 更完整。您可以选择使用哪一个。
- 本文配套的 *DirectX.Capture* 示例包含提高稳定性的新解决方案。仍然可能发生异常,但大多数异常可以通过重新设计此代码示例或通过更合适的方式捕获和处理异常来解决。请记住,此代码示例仅用于学习目的。它教您如何在 C# 中使用 DirectShow,并教您如何使用 GUI。出现的异常不应被视为问题,而应视为挑战!异常的主要优点是它能告诉您何时出现问题。作为副作用,程序会失败,通过调试,由于知道从哪里开始,因此可以更容易地找到问题的原因。
历史
- 2007 年 1 月 31 日:首次发布。
- 2007 年 8 月 1 日:添加了对 FM 收音机和视频去隔行的支持。通过视频设备过滤器捕获音频的解决方案已得到改进。此外,代码示例通过条件
DSHOWNET
支持DShowNET
或DirectShowLib
作为接口库。 - 2007 年 8 月 10 日:在另一个代码示例中添加了
SampleGrabber
和 VMR9 支持。 - 2007 年 11 月 28 日:修复了下载中的次要错误。
- 2008 年 2 月 17 日:次要文本修改,链接已更正,并且 *SampleGrabber* 示例已修改为支持 Visual Studio 2003 和 Visual Studio 2005。请仅使用其中一个版本。如果同时使用两个 Visual Studio 版本,请将代码放在不同的目录中!
- 2009 年 4 月 1 日:*SampleGrabber* 代码示例已移至新文章。次要文本修改,并修复了下载中的次要错误。