Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Features of DVB-T2 application development using C# and DirectShow.NET library

0.00/5 (No votes)
24 Sep 2014 1  
In this article I'm going to tell about the features of working with DVB-T2 TV tuners and the nuances that you can expect in this work.

Introduction

In this article I will tell about the features of working with DVB-T2 TV tuners and the nuances that you can expect in this work.

As the main library of interaction with the TV tuner I will use DirectShow.NET library.

Because the work with DVB-T2 tuners is just a little different from working with other DVB tuners - I will not describe the entire process of writing code to work with DVB, but I’ll try to focus on the nuances that may arise. Many of this nuances are relevant not only for DVB-T2, but also for DVB in general. So I hope this article will be interesting to people who are starting to develop programs working with DVB devices, but also have some experience with DirectShow and C# language of course.

DirectShow graph

DirectShow graph for the demo application will look like in the picture below (in the GraphEdit tool):

GraphEdit tool is a part of the Windows SDK, which you can download from the Microsoft website.

If you decide to draw a graph using GraphEdit manually - it will probably look like this:

Tip: When you build the graph using GraphEdit  manually  you can come across the following situation: attempt of connection of MPEG-2 Demultiplexer's video output pin (usually pin 003)  and Microsoft DTV-DVD Video Decoder input pin causes the error 0x80040217 "No combination of intermediate filters could be found to make the connection". Whilst connecting of MPEG-2 Demultiplexer's audio output pin (usually pin 007) and Microsoft DTV-DVD Audio Decoder input pin causes no problem. This problem stems from the fact that by default, the video output pins of the demultiplexer has MediaTypes which incompatible with the supported types of Microsoft DTV-DVD Video Decoder.

To "get around" this problem in GraphEdit, you should do the following steps:

  1. Build graph from Microsoft Network Provider Filter to the MPEG-2 Demultiplexer inclusive, without adding video decoder and video renderer filters.
  2. Call Microsoft Network Provider Filter properties. Fill in the definite DVB-T2 multiplex's frequency and bandwidth (in kHz). Then fill in the definite channel's SID. Submit Tune Request.
  3. Run the graph. If you have added audio decoder and audio output device to the graph - you would  hear the sound of the channel, which SID you have filled before
  4. After a few seconds - stop the graph.
  5. If your TuneRequest was correct and DVB signal was locked - the demultiplexer output pins would be configured to the correct MediaType automatically, and you would be able to connect the video output pin to the input pin of the video decoder.

Please note that the MPEG-2 Memultiplexer's output pin 001 should be connected with the BDA MPEG-2 Transport information filter. This filter provides information to the Microsoft Network Provider Filter filter for configuring other filters of the graph, including MPEG-2 Demultiplexer's output pin's MediaTypes.

Keep in mind that this method works only if you are using a generic Microsoft Network Provider filter in the graph, not "Microsoft DVB-T Network Provider" like in the demonstration program.

Building graph programmatically

Construction of the graph consists of the following steps:

  1. Insert "Microsoft DVB-T Network Provider" filter (or generic "Microsoft Network Provider" filter) into the graph and apply DVB-T tuning space to it
  2. Insert tuner device filters (Tuner filter and Capture filter), connect tuner filter to the provider filter, and capture filter to the tuner filter
  3. Insert "MPEG2-Demultiplexer" filter and connect it to the Capture filter
  4. Insert "BDA MPEG-2 Transport information filter" and "MPEG-2 Sections and Tables" filter and link them with the demultiplexer filter
  5. Clear all unused demultiplexer pins. Manually add Video and Audio output pins and configure them.
  6. Insert video and audio decoders and filters, link them with the demultiplexer filter.

Tip: For DVB-T2, as well as for the DVB-T, you should use DVB-T Network Provider as 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 recommends to use generic Microsoft Network Provider filter for any BDA graph. This provider is automatically adjusted depending on the type of tuner filter attached to it. However, I prefer to indicate the type of provider manually for the following reason: some DVB tuners support working with several digital broadcast standards at the same time (e.g., Beholder supports DVB-T and DVB-C standards). In this case, operating systems with such tuner driver installed would have several BDA tuner source filters. And any of them would connect to the generic provider successfully. However, I need to select only DVB-T filter and I want to do it automatically. Therefore, I try to connect each of available DVB source filters in series with configured DVB-T provider. Successful connection means that the correct filter was found.

// 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;
        }
    }
}

Tip: To connect the demultiplexer with video and audio decoders, you can create video and audio pins manually and configure them to the correct types.

//  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);

As you can see, video pin configuration is quite simple. Audio pin requires more detailed setup. In this example I have simplified creation of pins by setting them to the value types used by providers in my region for broadcast. You can configure the types of pins, depending on the video and audio stream information from MPEG-2 stream system tables (see below). Or you can provide pin type definition to the BDA MPEG-2 Transport information filter.

Tip: In the demo application I used Enhanced Video Renderer filter (EVR) as a video renderer.  You can use the renderer, which is more suited to your task. I've chosen EVR in order to show the features of working with it, as well as to tell about the issues of making video screenshots (see below).

Scanning the DVB multiplex and searching channels

Before tuning the graph to any TV/Radio channel,  you will need to get a list of available channels. To do it you should tune your graph to the desired frequency and bandwidth. Then run the graph. To get the multiplex service data you should use IDvbSiParser interface implementation. To initialize it you need to get IMpeg2Data interface, implemented in the MPEG-2 Sections and Tables filter.

Tip: Microsoft does not recommend using IMpeg2Data interface as outdated. Instead of this they advise to use IPSITables interface, implemented in the BDA MPEG-2 Transport information filter. However, IDviSiParser contains many methods to gain access to the required MPEG-2 stream service tables, including information about channels. These methods are very simple and useful, so I decided to use them for my demo. If someone is interested in it, I can do an article about using IPSITables interface in the future.

So we need to gather following information about each channel:

  1. Channel name - just to display it in the user interface
  2. Channel type (TV / Radio) - also just for the user interface
  3. SID - Channel Service ID - the main channel identifier
  4. Video and audio stream PIDs - MPEG-2 stream PIDs of video and audio stream.
  5. Stream ID (PLP ID) - DVB-T2 stream identifier. I will describe it more detail below.

Tip: In common case, to select the required channel it is enough only to know the channel SID (and PLP ID in some cases) in addition to the frequency and bandwidth of the multiplex. You don't need to tune multiplexer video/audio pins if you don't want  to do it: BDA MPEG-2 Transport information filter can make it for you. Nevertheless, I prefer to configure the video and audio PIDs manually.

All necessary channel information can be obtained using IDviSiParser methods: GetSDT, GetPAT and GetPMT. You can also gain more data using other IDviSiParser methods.

Tip: You should keep in mind that data from MPEG-2 service tables is broadcasted continuously with other data such as video and audio content streams. It means that you cannot be sure that the service information you need at the moment of query has already being broadcasted and received by your TV tuner, and therefore available for you. And so it is possible to request service information, which has not come to an MPEG-2 stream at the time of the request. A feature of the implementation of IDviSiParser is the impossibility of waiting for requested data to be received, or to set response wait timeout. Therefore, in order to ensure that the required channel information from the MPEG-2 stream is received, you will have to implement your own timeout functionality.

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;
}

Thus, in order to obtain a list of channels, it is necessary:

  1. Set the scanned stream ID (for Multiple PLP, see below),
  2. Put TuneRequest with frequency and bandwidth of the multiplex
  3. Read the service information through the IDviSiParser interface.

Of course, you should run your graph for these operations.

DVB-T2 stream ID (PLP ID)

DVB-T2 standard includes two operation modes: Single physical layer pipe (PLP) (Mode A) and Multiple PLP (Mode B). (You can read more at Wikipedia). If the content provider broadcasts the multiplex in multiple plp mode - you need the way to "select" required plp. Each plp stream usually has its own DVB settings, service lists, as well as the technical information. Each PLP stream has its own ID. Numbering always starts from zero. Thus, PLP ID can be considered as a necessary characteristic for making TuneRequest, like frequency and bandwidth.

I know at least three ways to work with the PLP ID:

  1. IDVBTLocator2 interface special methods  - get_PhysicalLayerPipeId and put_PhysicalLayerPipeId. This interface extends IDVBTLocator with these two methods and is designed specially for working with DVB-T2 standard. However, as practice shows, TV tuner manufacturers usually do not implement these methods in their drivers. I have not seen a single tuner, where these methods worked. If you want to develop software for a specific tuner model, then my advice is to contact the tuner manufacturer or dealer to clarify whether the tuner driver supports IDVBTLocator2 interface. By default, it is not a good idea to work with this interface.
  2. Vendor-specific driver properties. Usually all tuner manufacturers realize their property sets to work with their own software. Such properties are used through IKsProperySet interface methods. This applies not only to PLP ID setup. Always manufacturers implement lots of methods for convenient work with own hardware. However, these properties are clearly tied to specific brands of tuners. And there is another problem - the manufacturers do not like to share them with the public. Though of course there are exceptions.
  3. KSPROPERTY_BDA_DIGITAL_DEMODULATOR.KSPROPERTY_BDA_PLP_NUMBER property of the KSPROPSETID_BdaDigitalDemodulator property set. Most manufacturers implement this method in their drivers, so I’m going to use this method in my demo application.

Tip: Please note that the KSPROPERTY_BDA_PLP_NUMBER property is not described in MSDN. The KSPROPSETID_BdaDigitalDemodulator property set is described, but without some properties, including KSPROPERTY_BDA_PLP_NUMBER. 

The KSPROPERTY_BDA_PLP_NUMBER property, like other properties, are used via the IKsPropertySet interface implementation. However, in contrast to the properties that apply directly to the graph filters, this property, like all properties of the KSPROPSETID_BdaDigitalDemodulator property set, should be applied to the output pin of the tuner filter, which also implements IKsPropertySet.

// “tuner” is the DVB-T tuner filter in the filter graph
TunerPin = DsFindPin.ByDirection(tuner, PinDirection.Output, 0) as IKsPropertySet; 

There is another feature of the IKsPropertySet's methods invocation for filter pins. The KSP_NODE structure should be sent as the pInstanceData argument to Get and Set methods

// 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;
}

As I wrote above - KSPROPERTY_BDA_PLP_NUMBER property is not described in MSDN. But usually Get request returns the number of PLP streams in the multiplex, and Set request sets "active" PLP stream

Thus, a channel search operation is performed as follows:

  1. Set the frequency and bandwidth of the multiplex, then perform TuneRequest
  2. Determine the number of PLP streams in the multiplex
  3. Set active PLP ID = 0
  4. Perform TuneRequest
  5. Obtain channels information thru the IDvbSiParser interface.
  6. Increase active PLP ID = PLP ID + 1
  7. If the resulting PLP ID is equal to the total number of PLP streams (gathered at step 2)  - break the cycle
  8. Otherwise, go to step 4

I have come across some tuners which driver did not support the KSPROPERTY_BDA_PLP_NUMBER's Get query, but Set query worked properly. In this case, the multiplex scanning algorithm should be like this:

  1. Set active PLP ID = 0
  2. Perform TuneRequest
  3. Check for the broadcast signal presence (see. Below). If signal is missing - the cycle is aborted
  4. Obtain channels information thru the IDvbSiParser interface
  5. Increase active PLP ID = PLP ID + 1, go to step 2

Unfortunately, there are some cases when the tuner manufacturers are not implement support of KSPROPERTY_BDA_PLP_NUMBER property in the driver. In this case, you only have to contact the manufacturer and try to figure out a way to work with the PLP thru its tuner driver.

Determining signal presense, level and quality

This information can be obtained with the help of the methods described in the IBDA_SignalStatistics interface. To get implementation of this interface, you can use the tuner filter and the following code:

// 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);
            .....

Another way to obtain signal level and quality information is to use the KSPROPSETID_BdaSignalStats property set. The work with the property set is similar to work with the KSPROPSETID_BdaDigitalDemodulator property set. However, I came across a case when this property set support was not implemented in some drivers, that’s why I did not use it.

Channel selection

To select the desired channel you should:

  1. For Multiple PLP mode - set the channel PLP ID
  2. Map video and audio channel PIDs to the MPEG-2 Demultiplexer audio and video pins (for the radio channel - only audio pin of course)
  3. Generate TuneRequest, specifying frequency and bandwidth of the multiplex, as well as the channel SID
  4. Perform the TuneRequest.

There is an example of mapping multiplexer's video output pin to the channel's video 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;

Tip: Please note that you need to unmap previously mapped channel pid before mapping current channel's pid. To do this you need to store current channel PIDs. You cannot use IMPEG2PIDMap interface's handy EnumPIDMap method to list all pin PIDs and unmap them, because this method does not work in DirectShow.NET due to a DirectShow bug.

Rendering with Enhanced Video Renderer and making screenshots

For the first - why EVR. In one of my projects I had to get screenshots of video content and save them in files. The program I developed was a system service and did not have a UI. The most obvious solution was to use SampleGrabber filter and to get video frames from it. However, build-in SampleGrabber filter does not support VideoInfo2 stream type in its input pin. MPEG-2 Demultiplexer's video output pin type is VideoInfo2. Of course, it was possible to make my own SampleGrabber filter that would support VideoInfo2 input media type. But this decision seemed too “expensive” for me to implement, and I didn’t have enough time.

To make a screenshot you can also use standard video renderers such as Video Renderer Filter, which implements IBasicVideo interface, containing GetCurrentImage method. These renderers have three work modes - Windowed, Windowless and Renderless. The first two are not suitable, windowed - because of creating a separate content window, windowless - because it requires creating GUI element for rendering content. Renderless mode involves implementation of a number of interfaces. It takes a lot of time.

All these deficiencies are absent in Enhanced Video Renderer (EVR). All you need for screenshot task solving is to get access to the IMFVideoDisplayControl interface implementation. It can be done through EVR filter, as you can see below:

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 interface has a number of useful methods, including GetCurrentImage method. It allows you to obtain image content regardless of whether the filter is associated with the surface of the display or not.

To use Enhanced Video Renderer interfaces (including IMFVideoDisplayControl) you can use MediaFoundation.NET library.

Tip: IMFVideoDisplayControl.GetCurrentImage method returns a raw image content in device-independent bitmap (DIB) format and fills the BITMAPINFOHEADER structure with image params. To get the image in usable form (System.Drawing.Bitmap), you should create and fill BITMAPFILEHEADER structure (http://msdn.microsoft.com/en-us/library/aa930979.aspx), and write this structure, BITMAPINFOHEADER structure and received DIB data into the output stream consistently. (More information at http://en.wikipedia.org/wiki/BMP_file_format)

DirectShow rendering and WPF framework

The demonstration program was created using WPF framework. The visual elements of WPF do not provide access to the Win32 GUI Handles, which are needed to render the video content. There are several different ways to render DirectShow Video Renderer's data  in WPF, including the using of WindowsFormsHost proxy with  some Windows Forms surface, using SampleGrabber filter to extract images from the stream and rendering them on the WPF surface manually, and some others. In my example I used descendant of the HwndHost class. This class allows to create Win32 GUI element thru unmanaged code execution and to use this element as a part of WPF interface unit (however, there are some restrictions, but I will not write about it now, you can google about it if you want). All you need is to create an HwndHost descendant class and override it BuildWindowCore method:

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 class contains a Handle property, suitable for binding to the Video Renderer filter.

That's all

I hope this article was interesting for you. In the attached sample program you can see the realization of all features that I’ve mentioned in the article. If you have any questions, you can tell me and I’ll try to answer.

History

September 24, 2014: First release

September 25, 2014: Some fixes

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here