使用 DirectX.Capture 类库以 Windows Media Video 格式保存视频文件
对 DirectX.Capture 类进行增强,使用 IWMProfile 将音频和视频捕获到 Windows Media 文件。

引言
本文是我上一篇文章 DirectX.Capture 类的音频文件保存 的续篇。本文描述了以 Windows Media 文件格式 (WMV) 保存视频。保存视频比保存音频更复杂,因为保存视频可以包含音频也可以不包含音频。因此,由于与所选保存格式的冲突,视频保存失败的可能性更大。此外,预定义的视频格式比音频格式多得多,所以提供一种用户友好的方式来选择最适合您需求的特定视频格式会很有用。
DirectX.Capture
类示例对我在了解如何以 Windows Media 格式保存视频文件方面提供了很大帮助。由于 DirectX.Capture
类示例仅支持 AVI 文件保存,因此提供 Windows Media 文件保存是一项有趣的增强功能,因为文件可能会(大幅)变小。文章 Idael Cardoso 翻译的 C# Windows Media Format SDK 提供了关于 Windows Media Format SDK 的深刻见解。我花费了大量精力才弄清楚如何使用 IWMProfile
接口来检索有用信息。本文将提供一个在应用程序中使用 Windows Media 的示例。
使用配置文件...
为什么使用配置文件?嗯,每种 Windows Media 格式都由一个配置文件表示。Windows Media 有许多系统配置文件可供使用。有趣的是,还可以创建或修改现有配置文件。系统配置文件可以在 Windows 目录中的 WMSysPrx.prx 文件中找到。PRX 文件以 XML 格式包含配置文件的描述。也许您已经知道了,但 Microsoft 在 Windows Media Encoder 软件包中提供了一个名为 WMProEdt.exe 的 Windows Media 配置文件编辑器。值得一看。这是 Windows Media Encoder 配置文件的一部分
<profile version="589824"
storageformat="1"
name="Higher quality video (VBR 97)"
description="">
<streamconfig majortype="{73646976-0000-0010-8000-00AA00389B71}"
streamnumber="1"
streamname="Video Stream"
inputname="Video409"
bitrate="100000"
bufferwindow="-1"
reliabletransport="0"
decodercomplexity=""
rfc1766langid="en-us"
vbrenabled="1"
vbrquality="97"
bitratemax="0"
bufferwindowmax="0">
<videomediaprops maxkeyframespacing="80000000"
quality="0"/>
<wmmediatype subtype="{33564D57-0000-0010-8000-00AA00389B71}"
bfixedsizesamples="0"
btemporalcompression="1"
lsamplesize="0">
<videoinfoheader dwbitrate="100000"
dwbiterrorrate="0"
avgtimeperframe="333667">
...
</profile>
这是一个 XML 文件,包含配置文件的名称、描述、Windows 结构名称、属性名称及其值。因此,配置文件提供了有关支持的音频和视频流的所有详细信息。配置文件用于配置 ASF 文件写入器。ASF 文件写入器是负责实际多路复用、视频和/或音频编码以及实际文件保存的筛选器!通过配置 ASF 文件写入器,视频(和/或音频)将以选定的格式保存。但对我来说更重要的是,配置文件还可以用作用户友好地选择 Windows Media 文件格式的信息源。
了解 Windows Media 和配置文件的良好起点是在 MSDN 上阅读 **Windows Media Format SDK**。只需搜索这篇文章,链接会经常更新。该 SDK 包含示例、头文件、程序和文档。好消息是 Windows Media Format SDK 也可以下载。
使用 ASF 文件写入器时遇到的问题
在我上一篇关于音频保存的文章 DirectX.Capture 类的音频文件保存 中,我提到 ASF 文件写入器添加到图形时会产生冲突,但未连接。因此,我无法使用默认属性窗口来选择 Windows Media 格式进行文件保存。需要一个新窗体来提供与默认属性窗口类似的功能。这也没什么不好,因为现在可以显示比默认属性窗口更多的关于配置文件本身的信息。例如,可以显示有关支持音频和视频、比特率和配置文件描述的信息。甚至可以显示帧大小,但这需要更多工作,因为帧大小的检索有点困难。拥有特定的配置文件信息非常方便,因为您可以提前知道是否可以保存音频和/或视频。另一个好理由是程序也可以使用此信息。如果选定的格式与文件写入器的渲染音频和/或视频流不匹配,程序可以检测到并发出警告。
IWMProfile
要获取配置文件,需要使用 IWMProfile
接口。但是,还需要更多接口。首先,需要初始化配置文件管理器(IWMProfileManager
和 IWMProfileManager2
接口)。然后需要设置 Windows Media 版本;此处将使用 Windows Media 版本 8。使用 IWMProfileManager2
接口是因为通过此接口可以使用 GetSystemProfileCount()
、LoadProfileByID()
和 LoadSystemProfile()
来检索配置文件。
如果找到配置文件,则可以通过 GetName()
检索名称,并通过 GetDescription()
检索描述。要获取音频和视频流信息,还需要进行更多操作。使用 GetStreamCount()
可以检索流的数量。使用 GetStreamByNumber()
可以检索流配置信息(IWMStreamConfig
接口)。然后可以检查每个流是音频流还是视频流。帧大小更难获取;为此,需要视频信息头信息(VideoInfoHeader
)。没有直接的接口可以检索该信息,但有一个变通方法:使用 IWMWriter
接口。
IWMWriter
IWMWriter
接口允许访问 Windows Media 写入器。此写入器可用于将音频和视频流写入文件。虽然 ASF 文件写入器是一个筛选器,但 Windows Media 写入器不是筛选器,这是主要区别。为什么为此使用 Windows Media 写入器?嗯,它可以接受与 ASF 文件写入器相同的配置文件,并以相同的格式保存文件。此外,Windows Media 写入器可以通过输入属性间接访问视频信息头信息。可以通过 Windows Media 写入器的 GetInputProps()
检索输入属性。
如果找到视频输入属性,则可以通过 GetType()
和 GetMediaType()
(来自 IWMInputMediaProps
)检索视频信息,并将其存储在 AMMediaType
结构中。利用 AMMediaType
数据结构中的数据,可以访问 VideoInfoHeader
(如果可用)。由于其复杂性以及释放分配的 COM 对象的额外研究,在此示例中尚不支持使用 IWMWriter
。
另一个有趣的特性可能是使用自定义配置文件。使用 StreamReader
、IWMProfileManager
和 IConfigAsfWriter2
接口以及 LoadProfileByData
和 ConfigureFilterUsingProfile
函数,可以加载自己的配置文件。Stephen Toub 撰写的 MSDN 文章 "Fun with DVR-MS" 中描述了一个示例。
Using the Code
为了以 Windows Media 格式保存视频,引入了四个新类:WMLib
、WMProfileData
、AsfFormat
和 AsfForm
。WMProfileData
类用于存储配置文件数据。AsfFormat
类使用 WMProfileData
类来存储配置文件。AsfFormat
类具有一些将在本编码示例中使用的函数。AsfForm
类允许用户选择配置文件。AsfForm
类使用 AsfFormat
来存储和检索配置文件信息。WMLib
类提供了本编码示例中真正需要的 Windows Media Format SDK 接口。
我编写了 WMLib
类,因为我在文章 Idael Cardoso 翻译的 C# Windows Media Format SDK 中遇到的 Windows Media Format SDK 方面遇到了问题。当我完成示例时,我没有花时间再次使用 Yeti Windows Media SDK。欢迎您尝试使其正常工作,也许它会立即生效。主要问题是库大多未经测试。我不想在 DirectX.Capture
类示例中使用不需要的未经测试的接口。此外,一些结构和接口看起来与 DShowNet
中的结构和接口非常相似,这至少让我感到困惑。因此,我无法决定哪个库最好,于是我创建了自己的版本。因此,WMLib.cs 文件中的代码可能与 Yeti 版本以及非官方的 Sourceforge Windows Media 库(位于 CVS:directshownet/windowsmedialib,Sourceforge.net)非常相似。
WMProfileData
WMProfileData
类声明了存储配置文件数据的属性。应用程序可以使用这些数据来检索例如配置文件的名称、描述以及一些流信息。
public class WMProfileData
{
/// <summary> Name of the profile </summary>
protected string name;
/// <summary> Guid of the profile </summary>
protected Guid guid;
/// <summary> Description of the profile </summary>
protected string description;
/// <summary> Audio bit rate </summary>
protected int audioBitrate;
/// <summary> Video bit rate </summary>
protected int videoBitrate;
/// <summary>
/// Profile filename, a profile must have a guid or a filename
/// </summary>
protected string filename;
/// <summary> Indicates whether this profile supports an audio
/// stream
/// </summary>
protected bool audio;
/// <summary> Indicates whether this profile supports an video
/// stream
/// <summary>
protected bool video;
/// <summary> Indicates whether this profile is the one currently in
/// use </summary>
protected bool enabled;
}
AsfFormat
此类使用 WMProfileData
类来构建配置文件信息项列表。下一个示例展示了如何检索流类型和比特率。
hr = profile.GetStreamCount(out streamCount);
if((hr >= 0)&&((streamCount > 0))
{
IWMStreamConfig streamConfig = null;
Guid streamGuid = Guid.Empty;
audio = false;
video = false;
audioBitrate = 0;
videoBitrate = 0;
for(short i = 1;(i <= streamCount)&&(hr >= 0); i++)
{
hr = profile.GetStreamByNumber(i, out streamConfig);
if((hr >= 0)&&(streamConfig != null))
{
hr = streamConfig.GetStreamType(out streamGuid);
if(hr >= 0)
{
if(streamGuid == MediaType.Video)
{
video = true;
hr = streamConfig.GetBitrate(out videoBitrate);
if(hr < 0)
{
videoBitrate = 0;
}
}
else
if(streamGuid == MediaType.Audio)
{
audio = true;
hr = streamConfig.GetBitrate(out audioBitrate);
if(hr < 0)
{
audioBitrate = 0;
}
}
hr = 0; // Allow possible unreadable bitrates
}
}
} // for i
}
AsfFormat
类具有应用程序可以使用的多个函数。通过构造函数,可以初始化该类。通过 GetProfileFormatInfo()
函数,可以更新配置文件信息(例如,显示所有视频格式或显示不带音频的所有视频格式)。如果应用程序从音频文件保存切换到视频文件保存,此功能将非常有用。对于视频文件保存,菜单应显示支持至少一个视频流的配置文件列表。相反,对于音频文件保存,菜单应显示支持音频流且不支持视频流的配置文件列表。
要从文件读取配置文件,必须组合文件目录和文件名。首先读取程序文件目录,然后搜索具有 prx 文件扩展名的文件。以下代码检索 Windows Media 信息,用于构建带有 Asf 格式的菜单列表
// Look for profile (*.prx) files in the current directory.
// If found, then add the profile(s) to the list
string profileData;
string pathProfile = System.IO.Directory.GetCurrentDirectory();
string filterProfile = "*.prx";
// Obtain the file system entries in the directory path.
string[] directoryEntries =
System.IO.Directory.GetFileSystemEntries(pathProfile, filterProfile);
foreach (string filename in directoryEntries)
{
Debug.WriteLine(filename);
if(GetProfileDataFromFile(filename, out profileData))
{
hr = profileManager.LoadProfileByData(profileData, out profile);
if(hr >= 0)
{
if(AddProfileItem(avformat, profile, Guid.Empty, filename))
{
totalItems++;
}
}
}
}
如果选择了配置文件,将重新读取配置文件,并使用该配置文件来配置 Asf Writer。
/// <summary>
/// Configure profile from file to Asf file writer
/// </summary>
/// <param name="asfWriter"></param>
/// <param name="filename"></param>
/// <returns></returns>
public bool ConfigProfileFromFile(IBaseFilter asfWriter, string filename)
{
int hr;
//string profilePath = "test.prx";
// Set the profile to be used for conversion
if((filename != null)&&(File.Exists(filename)))
{
// Load the profile XML contents
string profileData;
using(StreamReader reader = new StreamReader(File.OpenRead(filename)))
{
profileData = reader.ReadToEnd();
}
// Create an appropriate IWMProfile from the data
// Open the profile manager
IWMProfileManager profileManager;
IWMProfile wmProfile = null;
hr = WMLib.WMCreateProfileManager(out profileManager);
if(hr >= 0)
{
// E.g. no tags
hr = profileManager.LoadProfileByData(profileData, out wmProfile );
}
if (profileManager != null)
{
Marshal.ReleaseComObject(profileManager);
profileManager = null;
}
// Config only if there is a profile retrieved
if(hr >= 0)
{
// Set the profile on the writer
IConfigAsfWriter configWriter = (IConfigAsfWriter)asfWriter;
hr = configWriter.ConfigureFilterUsingProfile(wmProfile);
if(hr >= 0)
{
return true;
}
}
}
return false;
}
AsfForm
AsfForm
类提供了一个窗体,该窗体提供有关所选配置文件的(有用)信息。此外,用户还可以选择其他配置文件。AsfForm
类作为 AsfForm.cs 文件添加到 CaptureTest
。
Capture.cs 中的代码更改
以下代码显示了 AsfFormat
构造函数和 GetProfileFormatInfo()
函数在 Capture
类中的可能用法。
switch(recFileMode)
{
case RecFileModeType.Wmv:
if(asfFormat == null)
{
asfFormat = new AsfFormat(AsfFormat.AsfFormatSelection.Video);
}
else
{
asfFormat.GetProfileFormatInfo(AsfFormat.AsfFormatSelection.Video);
}
break;
case RecFileModeType.Avi:
break;
default:
// Unsupported file format
return;
}
此外,还有一个单行代码可以更改文件名扩展名。
// Change filename extension
this.filename = Path.ChangeExtension(this.filename,
RecFileMode.ToString().ToLower());
在 renderGraph()
函数中,初始化了实际的文件保存。原始的 AVI 文件保存仍然可行。为了避免 Windows Media 格式的问题,视频压缩器仅用于 AVI 文件保存。对于 Windows Media 格式,将不使用视频压缩器。对于 Windows Media 格式,必须检索配置文件信息来配置 ASF 文件写入器。此外,还必须检索流信息。
// Render the file writer portion of graph (mux -> file)
// Record captured audio/video in Avi, Wmv or Wma format
Guid mediaSubType; // Media sub type
bool captureAudio = true;
bool captureVideo = true;
IBaseFilter videoCompressorfilter = null;
// Set media sub type and video compressor filter if needed
if(RecFileMode == RecFileModeType.Avi)
{
mediaSubType = MediaSubType.Avi;
// For Avi file saving a video compressor must be used
// If one is selected, that one will be used.
videoCompressorfilter = videoCompressorFilter;
}
else
{
mediaSubType = MediaSubType.Asf;
}
// Initialize the Avi or Asf file writer
hr = captureGraphBuilder.SetOutputFileName( ref mediaSubType, Filename,
out muxFilter, out fileWriterFilter );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
// For Wma (and Wmv) a suitable profile must be selected. This
// can be done via a property window, however the muxFilter is
// just created. if needed, the property windows should show up
// right now!
// Another solution is to configure the Asf file writer, the
// use interface must ensure the proper format has been selected.
if((RecFileMode == RecFileModeType.Wma)||
(RecFileMode == RecFileModeType.Wmv))
{
if(this.AsfFormat != null)
{
this.AsfFormat.UpdateAsfAVFormat(this.muxFilter);
this.AsfFormat.GetCurrentAsfAVInfo(out captureAudio,
out captureVideo);
}
}
为了保存视频,执行了视频流检查。此检查将防止在请求仅音频文件保存时进行视频文件保存。仅对于 AVI,将使用视频压缩器值。
// Render video (video -> mux) if needed or possible
if((VideoDevice != null)&&(captureVideo))
{
// Try interleaved first, because if the device supports it,
// it's the only way to get audio as well as video
cat = PinCategory.Capture;
med = MediaType.Interleaved;
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
videoDeviceFilter, videoCompressorfilter, muxFilter );
if( hr < 0 )
{
med = MediaType.Video;
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
videoDeviceFilter, videoCompressorfilter, muxFilter );
if ( hr == -2147220969 )
throw new DeviceInUseException( "Video device", hr );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
}
}
为了保存音频,执行了音频流检查。此检查将防止在请求仅视频文件保存时进行音频文件保存。
// Render audio (audio -> mux) if possible
if((AudioDevice != null)&&(captureAudio))
{
// If this Asf file format than please keep in mind that
// certain Wmv formats do not have an audio stream, so
// when using this code, please ensure you use a format
// which supports audio!
cat = PinCategory.Capture;
med = MediaType.Audio;
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
audioDeviceFilter, audioCompressorFilter, muxFilter );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
}
isCaptureRendered = true;
didSomething = true;
CaptureTest.cs 中的代码更改
在 CaptureTest.cs 中,需要修改 updateMenu()
函数以添加一个菜单,列出可能的音频/视频录制模式。添加了一个新函数 menuAVRecFileModes_Click()
;当用户选择菜单项时将调用此函数。
menuAVRecFileModes.MenuItems.Clear();
// Fill in all file modes, use enumerations also as string (and file
// extension)
for(int i = 0; i < 3; i++)
{
m = new MenuItem(((DirectX.Capture.Capture.RecFileModeType)i).ToString(),
new EventHandler(menuAVRecFileModes_Click));
m.Checked = (i == (int)capture.RecFileMode);
menuAVRecFileModes.MenuItems.Add(m);
}
menuAVRecFileModes.Enabled = true;
国家/地区相关设置
如果视频捕获设备支持电视调谐器,则会添加额外功能来初始化国家/地区相关设置。选择视频(或音频)设备后,可能会弹出一个窗口显示当前设置,并可以更新其值。这些设置,特别是国家/地区代码和视频标准,应设置正确,因为这些设置会影响电视调谐频率、音频和视频以及视频捕获的大小。

在美国,视频标准通常是 NTSC_M,模拟电视的视频尺寸为 720x480。在荷兰,PAL_B 是正常的视频标准,模拟电视的视频尺寸为 720x576。英国和比利时也使用 PAL 格式,但使用不同的模拟电视音频格式。设置不正确可能导致视频和/或音频质量差甚至没有。
关注点
此示例增加了在使用电视卡时获取可听声音的支持。它通过 PCI 总线提供音频,因此不需要有线音频连接。在测试过程中,我注意到经常没有列出音频源。如果存在源,我注意到它们无效,因此无法选择源。对于最后一个问题,我找到了两个解决方案。第一个解决方案是重新加载 AudioSources
和 VideoSources
,然后重新加载 AudioSource
和 VideoSource
。第二个解决方案是在 CrossbarSource.cs 中不释放 crossbar
对象(通过 Marshal.ReleaseComObject
)。这两种解决方案看起来都不太理想。在此示例中,由于其简单性,选择了第二种解决方案。要获取 AudioSources
列表,还需要进行一些修改。这些修改仅在通过音频设备无法找到 AudioSources
或通过视频设备找到音频设备时才需要。
我还对以 Mpeg2 格式保存文件进行了一些测试。关于此主题已经有一些有趣的文章,例如 BeCapture 撰写的 使用 MPEG2 捕获设备预览和录制 和 使用 Stream Buffer Engine - Windows XP Service Pack 1 上的 TIME SHIFT。第二篇文章非常详细地解释了 Mpeg2 解复用器应该如何配置。尽管如此,我仍未能使最后一个示例正常工作。看看我遇到的一些问题,我不确定这是否是一个有效的示例。尽管如此,这些示例帮助我得到了一个可行的 C# 解决方案。我担心的是,该解决方案非常依赖于可用的 Mpeg2 编码器和解码器筛选器。Graphedt 在检查组合是否可行方面很有帮助,但仍然很难获得好的结果。有时一个设计似乎可行,但文件仍然是零字节。有时需要修改筛选器的属性页面信息才能获得好的结果。我发现最容易用于文件保存的是 Nero 音频/视频编码器。使用硬件 Mpeg2 编码器也不太难。更难的是找到一个合适的转储筛选器。目前,我认为关于此主题的文章不会提供新信息。但是,我可以想象,对于将这些部分组合起来,一些帮助将非常有益。因此,如果您对此主题有疑问,请告诉我。
在测试过程中,我对 (ASF) 文件写入器筛选器中的老式 GetCurFile()
函数进行了一些调查。通过此函数,可以检索文件名和视频信息。文件名以字符串形式返回,视频信息以 AMMediaType
结构形式返回。有关 AMMediaType
的更多信息可以在 指定流格式 中找到。在检查 formatPtr
、formatSize
和 formatType
属性时,我发现这些信息似乎无法使用。我检查此函数是因为我曾想过也许可以通过此接口访问 VideoInfoHeader
。这将简化代码,并且不需要 IWMWriter
接口。不幸的是,格式数据看起来无法使用。对于在此示例中更改文件名,不需要视频信息。我还注意到(在 MSDN 上)GetCurFile()
可能使用 null
指针。在我看来,使用 null
而不是带有 formatPtr
的 AMMediaType
结构似乎更安全。
我注意到 DirectX.Capture
类示例(因此也包括我的这个示例版本)在程序启动并首次选择预览时通常会显示一张不错的画面。我不知道真正导致视频预览(通常是黑屏)变慢的原因。我注意到当调用 mediaControl.Stop
时会出现问题。实际上,预览引脚的处理方式似乎与捕获引脚略有不同。使用捕获引脚而不是预览引脚可获得最佳效果。这是渲染视频到默认视频渲染器的原始实现示例。
cat = PinCategory.Preview;
med = MediaType.Video;
hr = captureGraphBuilder.RenderStream( ref cat, ref med, videoDeviceFilter,
null, null );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
不幸的是,文件保存功能需要捕获引脚。使用 Video Mixing Renderer 9 进行视频渲染或使用较低的视频预览分辨率通常也能提供不错的画面。以下是使用 Video Mixing Renderer 9 作为视频渲染器的示例。
cat = PinCategory.Preview;
med = MediaType.Video;
IBaseFilter VMRfilter = (IBaseFilter) new VideoMixingRenderer9();
hr = graphBuilder.AddFilter(VMRfilter, "Video mixing renderer 9");
if( hr < 0 )
{
Marshal.ThrowExceptionForHR( hr );
}
hr = captureGraphBuilder.RenderStream(ref cat, ref med, videoDeviceFilter,
null, VMRfilter);
if( hr < 0 )
{
Marshal.ThrowExceptionForHR( hr );
}
我没有找到一个在所有捕获卡上都有效的解决方案,例如 Hauppauge PVR150、Pinnacle PCTV、MX460 Vivo、Radeon 8500 Vivo。Hauppauge 卡尤其给我带来了麻烦。在一台系统上它工作得很好,但在另一台系统上我通常会得到黑屏。
该代码示例已在 Visual Studio 2003 和 Visual Studio 2005 中进行了测试。这两个编译器版本之间可能会发生冲突。我添加了条件 VS2003
来显示代码差异。差异在于,在 Visual Studio 2003 中,一个信号名为 Closed
,而在 Visual Studio 2005 中,该信号名为 FormClosed
。此代码示例有两个版本:Visual Studio 2003 版本使用 DShowNET 作为 DirectShow 接口库。Visual Studio 2005 版本使用 DirectShowLib-2005 作为 DirectShow 接口库。仍然可以使用 DShowNET 配合 Visual Studio 2005,但我没有测试过。如果需要两个 Visual Studio 版本,请为该代码示例使用不同的目录以避免构建问题。我无意解决可能在几个 Visual Studio 版本之间发生的编码冲突和构建问题。
反馈和改进
我希望这段代码能帮助您理解 DirectX.Capture
类的结构。我也希望我提供了一个可能有用的增强功能。欢迎您发表评论和提问。
历史
- 2006 年 4 月 19 日:首次发布
- 2007 年 2 月 1 日:增加了通过视频设备筛选器捕获音频以及电视微调的支持。文章 DirectShow - 使用 C# 中的 IKsPropertySet 进行电视微调 更详细地描述了这些修改。
- 2007 年 8 月 1 日:增加了对 FM 收音机和视频隔行扫描的支持。通过视频设备筛选器捕获音频的解决方案已得到改进。此外,代码示例通过条件
DSHOWNET
支持DShowNET
或DirectShowLib
作为接口库。DirectX.Capture
类示例使用DShowNET
,而 DirectX.Capture 类库 (Refresh) 使用DirectShowLib
,后者比DShowNET
更完整。由您决定使用哪个。这些修改和代码示例的更详细描述可以在文章 DirectShow - 使用 C# 中的 IKsPropertySet 进行电视微调 中找到。 - 2007 年 8 月 10 日:在额外的代码示例中增加了
SampleGrabber
和 Video Mixing Renderer 9 的支持。目前,这个额外的代码示例在文章 DirectShow - 使用 C# 中的 IKsPropertySet 进行电视微调 中被描述为一个额外的代码示例。 - 2007 年 11 月 28 日:修复了源代码下载中的一个小错误
- 2008 年 2 月 17 日:文本小幅修改并更正了链接
- 2009 年 3 月 26 日:增加了从文件读取配置文件的支持。增加了更改颜色空间和视频标准的功能。增加了初始化国家/地区相关设置的功能。电视微调功能已从本代码示例中移除,以简化编码和测试。