使用 C# 和 DirectShow.NET 库进行 DVB-T2 应用程序开发的特性






4.97/5 (17投票s)
在本文中,我将介绍使用 DVB-T2 电视调谐器的工作特性以及可能遇到的细微差别。
引言
在本文中,我将介绍使用 DVB-T2 电视调谐器的工作特性以及可能遇到的细微差别。
作为与电视调谐器交互的主要库,我将使用 DirectShow.NET 库。
由于 DVB-T2 调谐器的工作与其他 DVB 调谐器的工作略有不同,因此我不会详细描述编写 DVB 工作代码的整个过程,而是会尝试专注于可能出现的细微差别。其中许多细微差别不仅适用于 DVB-T2,也适用于 DVB。因此,我希望本文对于刚开始开发 DVB 设备程序的开发者来说很有趣,当然,他们也应该对 DirectShow 和 C# 语言有一定的经验。
DirectShow 图
演示应用程序的 DirectShow 图在下面的图片中(在 GraphEdit 工具中)
GraphEdit 工具是 Windows SDK 的一部分,您可以从 Microsoft 网站下载。
如果您决定手动使用 GraphEdit 绘制图,它可能会看起来像这样
提示: 当您手动使用 GraphEdit 构建图时,可能会遇到以下情况:尝试连接 MPEG-2 Demultiplexer 的视频输出引脚(通常是 pin 003)和 Microsoft DTV-DVD Video Decoder 的输入引脚会导致错误 0x80040217“找不到中间滤波器组合来建立连接”。而在连接 MPEG-2 Demultiplexer 的音频输出引脚(通常是 pin 007)和 Microsoft DTV-DVD Audio Decoder 的输入引脚时,则不会出现问题。这个问题源于事实是,默认情况下,多路分解器的视频输出引脚的 MediaTypes 与 Microsoft DTV-DVD Video Decoder 支持的类型不兼容。
为了在 GraphEdit 中“绕过”这个问题,您应该执行以下步骤
- 从 Microsoft Network Provider Filter 到 MPEG-2 Demultiplexer(包括)构建图,但不要添加视频解码器和视频渲染器滤波器。
- 调用 Microsoft Network Provider Filter 的属性。填写特定的 DVB-T2 多路复用器的频率和带宽(以 kHz 为单位)。然后填写特定的频道 SID。提交调谐请求。
- 运行图。如果您已将音频解码器和音频输出设备添加到图中,您将听到您之前填写的 SID 频道的音频。
- 几秒钟后 - 停止图。
- 如果您的调谐请求正确且 DVB 信号已锁定 - 多路分解器的输出引脚将自动配置为正确的 MediaType,您就可以将视频输出引脚连接到视频解码器的输入引脚。
请注意,MPEG-2 Memultiplexer 的输出引脚 001 应与 BDA MPEG-2 Transport information filter 连接。此滤波器为 Microsoft Network Provider Filter 滤波器提供配置图中其他滤波器(包括 MPEG-2 Demultiplexer 的输出引脚的 MediaTypes)的信息。
请注意,此方法仅在您在图中使用了通用 Microsoft Network Provider filter 时才有效,而不是像演示程序中那样使用“Microsoft DVB-T Network Provider”。
以编程方式构建图
图的构建包含以下步骤
- 将“Microsoft DVB-T Network Provider”滤波器(或通用的“Microsoft Network Provider”滤波器)插入图中并为其应用 DVB-T 调谐空间
- 插入调谐器设备滤波器(Tuner 滤波器和 Capture 滤波器),将调谐器滤波器连接到提供者滤波器,将捕获滤波器连接到调谐器滤波器
- 插入“MPEG2-Demultiplexer”滤波器并将其连接到捕获滤波器
- 插入“BDA MPEG-2 Transport information filter”和“MPEG-2 Sections and Tables”滤波器,并将它们与 demultiplexer 滤波器链接
- 清除所有未使用的多路分解器引脚。手动添加视频和音频输出引脚并进行配置。
- 插入视频和音频解码器及滤波器,并将它们与多路分解器滤波器链接。
提示: 对于 DVB-T2 以及 DVB-T,您应该使用 DVB-T Network Provider 作为 BDA Network provider。
Guid networkProviderClsid;
// I set up TunungSpace previously, including network type
var hr = TuningSpace.get__NetworkType(out networkProviderClsid);
DsError.ThrowExceptionForHR(hr);
// Inserting "Microsoft DVB-T Network Provider" filter
networkProvider = FilterGraphTools.AddFilterFromClsid(
graphBuilder,
networkProviderClsid,
"Microsoft DVB-T Network Provider");
Microsoft 建议在任何 BDA 图中使用通用的 Microsoft Network Provider filter。此提供者会根据连接到它的调谐器滤波器的类型自动调整。然而,我更倾向于手动指定提供者类型,原因如下:一些 DVB 调谐器支持同时处理多种数字广播标准(例如,Beholder 支持 DVB-T 和 DVB-C 标准)。在这种情况下,安装了此类调谐器驱动程序的操作系统将有多个 BDA 调谐器源滤波器。其中任何一个都可以成功连接到通用提供者。但是,我只想选择 DVB-T 滤波器,并且希望自动完成。因此,我尝试将每个可用的 DVB 源滤波器串联连接到配置好的 DVB-T 提供者。成功连接意味着找到了正确的滤波器。
// Enum all BDA source filters
foreach (var device in DsDevice.GetDevicesOfCat(FilterCategory.BDAReceiverComponentsCategory))
{
try
{
// Add BDA filter to graph
capture = FilterGraphTools.AddFilterByName(
graphBuilder,
FilterCategory.BDAReceiverComponentsCategory,
device.Name);
if (capture == null)
throw new Exception("Failed to create capture filter");
// Try to connect DBA source filter to the provider filter
FilterGraphTools.ConnectFilters(
graphBuilder,
DsFindPin.ByDirection(tuner, PinDirection.Output, 0),
DsFindPin.ByDirection(capture, PinDirection.Input, 0), false);
}
catch
{
// BDA filter is not DVB-T
if (capture != null)
{
// Remove filter from graph
graphBuilder.RemoveFilter(capture);
// Release filter's COM object
Marshal.ReleaseComObject(capture);
capture = null;
}
}
}
提示: 要将多路分解器与视频和音频解码器连接,您可以手动创建视频和音频引脚,并将其配置为正确的类型。
// Setup video pin media type
var videoPinType = new AMMediaType
{
majorType = MediaType.Video,
subType = MediaSubType.H264,
formatType = FormatType.VideoInfo2
};
IPin videoPin;
var hr = ((IMpeg2Demultiplexer)mpeg2Demux).CreateOutputPin(videoPinType, "Video", out videoPin);
DsError.ThrowExceptionForHR(hr);
// Setup audio pin media type
var audioPinType = new AMMediaType
{
majorType = MediaType.Audio,
subType = MediaSubType.Mpeg2Audio,
sampleSize = 65536,
temporalCompression = false,
fixedSizeSamples = true, // or false in MediaPortal //true
unkPtr = IntPtr.Zero,
formatType = FormatType.WaveEx
};
// We need to set up FormatPtr for proper connection to decoder filter
var mpeg1WaveFormat = new MPEG1WaveFormat
{
wfx = new DirectShowLib.WaveFormatEx
{
wFormatTag = 0x0050,
nChannels = 2,
nSamplesPerSec = 48000,
nAvgBytesPerSec = 32000,
nBlockAlign = 768,
wBitsPerSample = 0,
cbSize = 22 // extra size
},
fwHeadLayer = AcmMpegHeadLayer.Layer2,
//dwHeadBitrate = 0x00177000,
fwHeadMode = AcmMpegHeadMode.SingleChannel,
fwHeadModeExt = 1,
wHeadEmphasis = 1,
fwHeadFlags = AcmMpegHeadFlags.OriginalHome | AcmMpegHeadFlags.IDMpeg1,
dwPTSLow = 0,
dwPTSHigh = 0
};
audioPinType.formatSize = Marshal.SizeOf(mpeg1WaveFormat);
audioPinType.formatPtr = Marshal.AllocHGlobal(audioPinType.formatSize);
Marshal.StructureToPtr(mpeg1WaveFormat, audioPinType.formatPtr, false);
IPin audioPin;
hr = ((IMpeg2Demultiplexer)mpeg2Demux).CreateOutputPin(audioPinType, "Audio", out audioPin);
DsError.ThrowExceptionForHR(hr);
正如您所见,视频引脚配置相当简单。音频引脚需要更详细的设置。在此示例中,我通过将引脚设置为我所在地区提供者用于广播的值类型来简化了引脚的创建。您可以根据 MPEG-2 流系统表中的视频和音频流信息(见下文)配置引脚类型。或者,您可以向 BDA MPEG-2 Transport information filter 提供引脚类型定义。
提示: 在演示应用程序中,我使用了 Enhanced Video Renderer filter (EVR) 作为视频渲染器。您可以根据您的任务使用更合适的渲染器。我选择 EVR 是为了展示使用它的特性,并讨论制作视频截图的问题(见下文)。
扫描 DVB 多路复用器和搜索频道
在将图调谐到任何电视/广播频道之前,您需要获取可用频道列表。要做到这一点,您应该将图调谐到所需频率和带宽。然后运行图。要获取多路复用服务数据,您应该使用 IDvbSiParser 接口的实现。要初始化它,您需要获取 IMpeg2Data 接口,该接口在 MPEG-2 Sections and Tables filter 中实现。
提示: Microsoft 不建议使用 IMpeg2Data 接口,因为它已过时。取而代之的是,他们建议使用 IPSITables 接口,该接口在 BDA MPEG-2 Transport information filter 中实现。但是,IDviSiParser 包含许多访问所需的 MPEG-2 流服务表的方法,包括频道信息。这些方法非常简单且有用,因此我决定在我的演示中使用它们。如果有人对此感兴趣,我将来可以写一篇关于使用 IPSITables 接口的文章。
因此,我们需要收集每个频道的以下信息
- 频道名称 - 仅用于在用户界面中显示
- 频道类型(电视/广播) - 也仅用于用户界面
- SID - 频道服务 ID - 主要频道标识符
- 视频和音频流 PID - 视频和音频流的 MPEG-2 流 PID。
- 流 ID (PLP ID) - DVB-T2 流标识符。我将在下面更详细地介绍它。
提示: 在一般情况下,要选择所需的频道,除了频率和多路复用器的带宽之外,只需知道频道 SID(以及某些情况下的 PLP ID)就足够了。如果您不想这样做,则无需调谐多路复用器的视频/音频引脚:BDA MPEG-2 Transport information filter 可以为您完成。尽管如此,我还是倾向于手动配置视频和音频 PID。
所有必要的频道信息都可以通过 IDviSiParser 的方法获得:GetSDT、GetPAT 和 GetPMT。您还可以通过其他 IDviSiParser 方法获取更多数据。
提示: 您应该记住,MPEG-2 服务表的数据与其他数据(如视频和音频内容流)一起连续广播。这意味着您不能确定您在查询时需要的服务信息是否已广播并被您的电视调谐器接收,因此是否可用。所以有可能请求在请求时尚未出现在 MPEG-2 流中的服务信息。 IDviSiParser 实现的一个特点是无法等待请求的数据被接收,或设置响应等待超时。因此,为了确保接收到 MPEG-2 流中所需的频道信息,您将不得不实现自己的超时功能。
while (true)
{
// Get requested data from MPEG-2 stream
var result = parser.GetPMTandPAT(aModel);
// Success - return data
if (result != null)
return result;
// Check if timeout occured
if (timeElapsed >= QueryTimeout)
return null;
// Wait some time
Thread.Sleep(QueryIterationPause);
timeElapsed += QueryIterationPause;
}
因此,为了获取频道列表,需要
- 设置扫描的流 ID(对于 Multiple PLP,请参阅下文),
- 使用多路复用器的频率和带宽进行 TuneRequest
- 通过 IDviSiParser 接口读取服务信息。
当然,您应该为这些操作运行您的图。
DVB-T2 流 ID (PLP ID)
DVB-T2 标准包含两种操作模式:单物理层管道 (PLP)(模式 A)和多 PLP(模式 B)。(您可以在 Wikipedia 上阅读更多内容)。如果内容提供商以多 PLP 模式广播多路复用器 - 您需要一种方法来“选择”所需的 PLP。每个 PLP 流通常有自己的 DVB 设置、服务列表以及技术信息。每个 PLP 流都有自己的 ID。编号始终从零开始。因此,PLP ID 可以被认为是进行 TuneRequest 的必要特征,类似于频率和带宽。
我知道至少有三种方法可以处理 PLP ID
- IDVBTLocator2 接口的特殊方法 - get_PhysicalLayerPipeId 和 put_PhysicalLayerPipeId。此接口使用这两个方法扩展了 IDVBTLocator,并且专门用于处理 DVB-T2 标准。然而,实践证明,电视调谐器制造商通常不在其驱动程序中实现这些方法。我还没有见过任何一个调谐器,这些方法是有效的。如果您想为特定的调谐器型号开发软件,那么我的建议是联系调谐器制造商或经销商,以澄清调谐器驱动程序是否支持 IDVBTLocator2 接口。默认情况下,使用此接口并不是一个好主意。
- 特定于供应商的驱动程序属性。通常,所有调谐器制造商都实现了其属性集,以与自己的软件配合使用。此类属性通过 IKsProperySet 接口的方法使用。这不仅适用于 PLP ID 设置。制造商通常会实现许多方法,以便方便地使用自己的硬件。然而,这些属性明显与特定品牌的调谐器相关联。还有另一个问题 - 制造商不喜欢公开它们。当然也有例外。
- KSPROPSETID_BdaDigitalDemodulator 属性集中的 KSPROPERTY_BDA_DIGITAL_DEMODULATOR.KSPROPERTY_BDA_PLP_NUMBER 属性。大多数制造商在其驱动程序中实现了此方法,因此我将在我的演示应用程序中使用此方法。
提示: 请注意,KSPROPERTY_BDA_PLP_NUMBER 属性在 MSDN 中没有描述。 KSPROPSETID_BdaDigitalDemodulator 属性集有描述,但缺少一些属性,包括 KSPROPERTY_BDA_PLP_NUMBER。
KSPROPERTY_BDA_PLP_NUMBER 属性,与其他属性一样,通过 IKsPropertySet 接口的实现来使用。但是,与直接应用于图滤波器的属性不同,此属性以及 KSPROPSETID_BdaDigitalDemodulator 属性集的所有属性都应应用于实现 IKsPropertySet 的调谐器滤波器的输出引脚。
// “tuner” is the DVB-T tuner filter in the filter graph
TunerPin = DsFindPin.ByDirection(tuner, PinDirection.Output, 0) as IKsPropertySet;
调用滤波器引脚的 IKsPropertySet 方法还有一个特性。 KSP_NODE 结构应作为 pInstanceData 参数传递给 Get 和 Set 方法。
// Gets the property value (ksGuid property set and its ksParam) from ksTarget
public static object KSGetNode(IKsPropertySet ksTarget, Guid ksGuid, int ksParam, Type ksType)
{
object obj;
var dataPtrSize = Marshal.SizeOf(ksType);
var dataPtr = Marshal.AllocCoTaskMem(dataPtrSize);
var instancePtrSize = Marshal.SizeOf(typeof(KSP_NODE));
var instancePtr = Marshal.AllocCoTaskMem(instancePtrSize);
try
{
int cbBytes;
var result = ksTarget.Get(
ksGuid,
ksParam,
instancePtr,
instancePtrSize,
dataPtr,
dataPtrSize,
out cbBytes);
if (result != 0)
throw new Exception(
string.Format("KSPropertySet KSP_NODE GET method failed [{0:X}]",
result));
obj = Marshal.PtrToStructure(dataPtr, ksType);
}
finally
{
if (dataPtr != IntPtr.Zero)
Marshal.FreeCoTaskMem(dataPtr);
if (instancePtr != IntPtr.Zero)
Marshal.FreeCoTaskMem(instancePtr);
}
return obj;
}
如上所述 - MSDN 中没有描述 KSPROPERTY_BDA_PLP_NUMBER 属性。但是通常 Get 请求返回多路复用器中 PLP 流的数量,而 Set 请求设置“活动”PLP 流。
因此,频道搜索操作执行如下
- 设置多路复用器的频率和带宽,然后执行 TuneRequest
- 确定多路复用器中 PLP 流的数量
- 设置活动 PLP ID = 0
- 执行 TuneRequest
- 通过 IDvbSiParser 接口获取频道信息。
- 增加活动 PLP ID = PLP ID + 1
- 如果结果 PLP ID 等于 PLP 流的总数(在步骤 2 中收集)- 则跳出循环
- 否则,转到步骤 4
我遇到过一些调谐器,其驱动程序不支持 KSPROPERTY_BDA_PLP_NUMBER 的 Get 查询,但 Set 查询正常工作。在这种情况下,多路复用器扫描算法应如下进行
- 设置活动 PLP ID = 0
- 执行 TuneRequest
- 检查广播信号是否存在(见下文)。如果信号丢失 - 则中止循环
- 通过 IDvbSiParser 接口获取频道信息
- 增加活动 PLP ID = PLP ID + 1,转到步骤 2
不幸的是,在某些情况下,调谐器制造商未在驱动程序中实现对 KSPROPERTY_BDA_PLP_NUMBER 属性的支持。在这种情况下,您只能联系制造商并尝试弄清楚如何通过其调谐器驱动程序处理 PLP。
确定信号存在、级别和质量
可以通过 IBDA_SignalStatistics 接口中描述的方法来获取此信息。要获取此接口的实现,您可以使用调谐器滤波器和以下代码
// Get IBDA_Topology interface
var topology = (IBDA_Topology)tuner;
int nodeTypesCount;
var nodeType = new int[64];
var ifs = new Guid[64];
// Get topoloye node types count
topology.GetNodeTypes(out nodeTypesCount, 64, nodeType);
for (var i = 0; i < nodeTypesCount; i++)
{
int interfaces;
// Get node interfaces
topology.GetNodeInterfaces(nodeType[i], out interfaces, 64, ifs);
for (var j = 0; j < interfaces; j++)
{
// Check for IBDA_SignalStatistics interface
if (ifs[j] == typeof(IBDA_SignalStatistics).GUID)
{
// Success - extracting interface
object ostats;
topology.GetControlNode(0, 1, nodeType[i], out ostats);
var stats = (IBDA_SignalStatistics)ostats;
// Now you can call requested methods
int strength;
int quality;
bool present;
bool locked;
// Signal strength
stats.get_SignalStrength(out strength);
// Signal quality
stats.get_SignalQuality(out quality);
// Signal presence
stats.get_SignalPresent(out present);
// Signal locked flag
stats.get_SignalLocked(out locked);
.....
获取信号级别和质量信息的另一种方法是使用 KSPROPSETID_BdaSignalStats 属性集。使用此属性集与使用 KSPROPSETID_BdaDigitalDemodulator 属性集类似。但是我遇到过一些驱动程序未实现此属性集支持的情况,因此我没有使用它。
频道选择
要选择所需的频道,您应该
- 对于 Multiple PLP 模式 - 设置频道 PLP ID
- 将视频和音频频道 PID 映射到 MPEG-2 Demultiplexer 的音频和视频引脚(对于广播频道 - 当然只有音频引脚)
- 生成 TuneRequest,指定多路复用器的频率和带宽,以及频道 SID
- 执行 TuneRequest。
下面是如何将多路复用器的视频输出引脚映射到频道视频 PID 的示例
// Get Video Pin
var pid = (IMPEG2PIDMap)DsFindPin.ByName(mpeg2Demux, "Video");
int hr;
if (lastUsedVideoPid > 0)
{
// Unmap previously mapped PID
hr = pid.UnmapPID(1, new[] { lastUsedVideoPid });
DsError.ThrowExceptionForHR(hr);
}
if (pidValue > 0)
{
// Map new PID
hr = pid.MapPID(1, new[] { pidValue }, MediaSampleContent.ElementaryStream);
DsError.ThrowExceptionForHR(hr);
}
lastUsedVideoPid = pidValue;
提示: 请注意,您需要取消映射先前映射的频道 PID,然后才能映射当前频道的 PID。要做到这一点,您需要存储当前的频道 PID。您不能使用 IMPEG2PIDMap 接口方便的 EnumPIDMap 方法来列出所有引脚 PID 并取消映射它们,因为由于 DirectShow 的错误,此方法在 DirectShow.NET 中不起作用。
使用 Enhanced Video Renderer 进行渲染和截图
首先 - 为什么选择 EVR。在我的一项项目中,我需要截取视频内容的截图并将其保存到文件中。我开发的程序是一个系统服务,没有用户界面。最明显的解决方案是使用 SampleGrabber filter 并从中获取视频帧。然而,内置的 SampleGrabber filter 在其输入引脚上不支持 VideoInfo2 流类型。MPEG-2 Demultiplexer 的视频输出引脚类型是 VideoInfo2。当然,我可以制作自己的 SampleGrabber 滤波器,它会支持 VideoInfo2 输入媒体类型。但对我来说,这个解决方案实现起来“代价”太高,而且我没有足够的时间。
要截屏,您还可以使用标准的视频渲染器,例如 Video Renderer Filter,它实现了 IBasicVideo 接口,其中包含 GetCurrentImage 方法。这些渲染器有三种工作模式 - Windowed, Windowless 和 Renderless。前两种不适合,窗口化 - 因为它创建了一个单独的内容窗口,无窗口化 - 因为它需要创建一个用于渲染内容的 GUI 元素。无渲染模式涉及实现一系列接口。这需要花费很多时间。
所有这些缺点在 Enhanced Video Renderer (EVR) 中都不存在。您需要做的就是获取对 IMFVideoDisplayControl 接口实现的访问权限。如您下面所见,这可以通过 EVR 滤波器完成
object o;
var service = (IMFGetService)videoRenderer; // <- This is our EVR filter
service.GetService(MFServices.MR_VIDEO_RENDER_SERVICE, typeof(IMFVideoDisplayControl).GUID,
out o);
displayControl = (IMFVideoDisplayControl)o; // Voila!
IMFVideoDisplayControl 接口有许多有用的方法,包括 GetCurrentImage 方法。它允许您获取图像内容,而无论滤波器是否与显示表面关联。
要使用 Enhanced Video Renderer 接口(包括 IMFVideoDisplayControl),您可以使用 MediaFoundation.NET 库。
提示: IMFVideoDisplayControl.GetCurrentImage 方法以设备无关位图 (DIB) 格式返回原始图像内容,并用图像参数填充 BITMAPINFOHEADER 结构。要获取可用格式的图像(System.Drawing.Bitmap),您需要创建并填充 BITMAPFILEHEADER 结构(http://msdn.microsoft.com/en-us/library/aa930979.aspx),并将此结构、BITMAPINFOHEADER 结构和接收到的 DIB 数据一致地写入输出流。(更多信息请参阅 http://en.wikipedia.org/wiki/BMP_file_format)。
DirectShow 渲染和 WPF 框架
演示程序是使用 WPF 框架创建的。WPF 的视觉元素不提供对需要渲染视频内容的 Win32 GUI 句柄的访问。有几种不同的方法可以在 WPF 中渲染 DirectShow Video Renderer 的数据,包括使用 WindowsFormsHost 代理和一些 Windows Forms 曲面,使用 SampleGrabber 滤波器从流中提取图像并在 WPF 曲面上手动渲染它们,以及其他一些方法。在我的例子中,我使用了 HwndHost 类的子类。此类允许通过非托管代码执行来创建 Win32 GUI 元素,并使用该元素作为 WPF 界面单元的一部分(但是,有一些限制,但现在我不会写这些,如果您愿意,可以自己搜索)。您需要做的就是创建一个 HwndHost 的子类,并覆盖其 BuildWindowCore 方法。
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var hostHeight = (int)ActualHeight;
var hostWidth = (int)ActualWidth;
var hwndHost = CreateWindowEx(0, "static", "",
WS_CHILD | WS_VISIBLE,
0, 0,
hostHeight, hostWidth,
hwndParent.Handle,
(IntPtr)HOST_ID,
IntPtr.Zero,
0);
return new HandleRef(this, hwndHost);
}
HwndHost 类包含一个 Handle 属性,适合绑定到 Video Renderer 滤波器。
就是这样
我希望这篇文章对您有所启发。在随附的示例程序中,您可以看到文章中提到的所有特性的实现。如果您有任何问题,可以告诉我,我会尽力回答。
历史
2014年9月24日:首次发布
2014年9月25日:一些修复