Programming D-VHS and MPEG-2 Devices in DirectShow

December 2003

 

**Applies to:
**   Microsoft® Windows® XP and later.

Download the code sample.

Summary: Describes how to write DirectShow applications in C++ for D-VHS recorders and MPEG-2 camcorders. Includes source code for a sample application demonstrating the concepts discussed in the article. The article assumes a basic understanding of DirectShow application programming. (10 printed pages)

Contents

Introduction
Running the Sample Application
Technical Overview
Application Steps
Conclusion
For More Information

Introduction

D-VHS recorders and MPEG-2 camcorders are two kinds of consumer device that capture video in MPEG-2 format. D-VHS recorders are similar to traditional VCRs, but their digital format makes them well suited for digital television. MPEG-2 camcorders are similar to DV camcorders. These devices are supported on Microsoft Windows® XP through the MSTape driver (Mstape.sys). This article describes how to write Microsoft® DirectShow® applications for these types of device.

A sample C++ application is included with this article, to demonstrate the programming tasks described here. The application previews audio and video from an MPEG-2 device connected to your computer. The audio and video streams may be live, or captured from the device's tape transport. If you have written applications for other kinds of video capture device, a lot of the code in the sample application will be familiar to you. This article focuses on the requirements unique to MPEG-2 devices, such as getting program information from the MPEG-2 transport stream and configuring the MPEG-2 Demultiplexer filter.

You can download the source code for the sample application at DVHS.exe sample.

Requirements

  • Microsoft Windows XP.
  • A third-party MPEG-2 decoder.
  • Microsoft DirectX® SDK version 9.0 or later.

Running the Sample Application

The sample application provided with this article was tested on a D-VHS recorder, but the principles are the same for MPEG-2 camcorders. The application may require modifications to work with some devices; details are given in the article. Before running the application, connect your device to your computer. The details will depend on the model of device. In my case, I connected a coaxial cable from the cable television outlet to the D-VHS recorder, and then connected the recorder to my computer through the IEEE 1394 port. For D-VHS recorders, it may be useful to connect the recorder to a television set first, to make sure you are getting a signal.

Next, build and register the PSI Parser sample from the DirectShow SDK. This sample is located in the Samples\C++\DirectShow\Filters\PSIParser directory of the SDK. The project builds a DLL named PsiParser.ax. Use the Regsvr32 utility to register this DLL.

Now compile and run the sample application. It should look like Figure 1. Assuming that Windows has detected your device correctly, the device will appear in the D-VHS/MPEG-2 Devices list box. Select the device name and click Select Device. The application now begins to listen for program information from the device. (The program information is part of the MPEG-2 stream, as described in the next section). When it receives program information, it populates the Programs list. If you then select a program from the list and click View Program, the application attempts to render the audio and video streams in the program.

Note   If the application fails to render one or both streams, it may not recognize the stream type, or your MPEG-2 decoder may not accept the format that the application specified. For more information, refer to Configure the Audio and Video Pins later in this article. Also, be aware that some MPEG-2 decoders do not function if they are loaded while an application is running in a debugger.

D-VHS Sample Application

Figure 1. Sample application screen shot

Technical Overview

To understand D-VHS and MPEG-2 devices, you need to know a little about the MPEG-2 Systems layer, the set of specifications that defines how MPEG-2 audio and video streams are multiplexed. The MPEG-2 Systems layer defines two kinds of multiplexed MPEG-2 streams, program streams and transport streams. D-VHS and MPEG-2 devices use transport streams.

Before they are multiplexed, the MPEG-2 audio and video streams start as byte streams, called elementary streams. Each stream is packetized to form a packetized elementary stream (PES). The transport stream (TS) adds a second layer of packetizing, in which the PES packets are broken up and placed into TS packets.

Each elementary stream in the transport stream is assigned a number, called a program identifier (PID). The PID enables the demultiplexer to identify which TS packets correspond to which streams. The transport stream also carries tables of information about the elementary streams. Collectively, these tables are called program specific information (PSI). The PSI tables are carried in TS packets, just like PES packets. The MPEG-2 standard defines several types of PSI data, only two of which are relevant for this discussion: program map tables and program association tables.

  • A program map table (PMT) defines a single program. It contains a list of streams for that program. The table entries in the PMT list the PID assignment for each stream, along with a code that identifies the stream type.
  • The program association table (PAT) contains a list of the program map tables. Each entry is a PID that identifies the PMT packets for one program. The PAT is always assigned to PID 0x00, so the demultiplexer can locate it in the transport stream, and then extract the other PID assignments from the PMTs.

Figure 2 shows these relationships. The specific PID values were invented for this example, except for the PAT, which is always assigned to PID 0x00.

MPEG-2 Transport Stream tables

Figure 2. Transport stream PSI tables

Note that other than PID 0x00, the multiplexer is free to assign any PID values to the various PMTs and elementary streams. Therefore, an application must discover these values by parsing the PSI data. The PSI Parser sample in the DirectShow SDK is a filter that does just that. It supports a custom interface, IMpeg2PsiParser, for the application to retrieve the program data.

Application Steps

Here are the basic steps that an application must perform to play content from a D-VHS or MPEG-2 device:

  1. Enumerate video capture devices and locate the D-VHS or MPEG-2 device.
  2. Create a DirectShow capture filter from the device moniker.
  3. Connect the capture filter to the MPEG-2 Demultiplexer ("demux") filter.
  4. Create a new output pin on the demux filter. Map PID 0x00 to this pin, and connect the pin to the PSI Parser filter.
  5. Run the filter graph and listen for EC_PROGRAM_CHANGED events.
  6. Use the IMpeg2PsiParser interface to find the PIDs and stream types for the program streams.
  7. Stop the filter graph.
  8. Create new pins on the demux filter for the audio and video elementary streams.
  9. Render the audio and video pins and restart the filter graph.

The remainder of this article describes these steps in more detail.

Locate the D-VHS or MPEG-2 Device

To locate the device, create the system device enumerator and enumerate the video capture devices on the system. The system device enumerator returns a list of monikers that represent capture devices. For details, see "Selecting a Capture Device" in the DirectShow SDK. The relevant code in the sample application is the CMainDialog::FindDevices method in MainDialog.cpp.

Next, you need to identify the MSTape driver in the list of video capture devices. The device moniker for the MSTape driver supports a property named DeviceClassGUID. This property returns the string representation of the GUID value MSTapeGUID. You can use this property to identify the MSTape driver, as shown in the following code:

CComVariant var;
hr = pPropBag->Read(OLESTR("DeviceClassGUID"), &var, NULL);
if (SUCCEEDED(hr) && var.vt == VT_BSTR)
{
    // The DeviceClassGUID property is a string representation
    // of a GUID. We compare this against MSTapeDeviceGUID.
    CComBSTR bstrGUID(MSTapeDeviceGUID);
    CComBSTR bstr(var.bstrVal);
    bstrGUID.ToUpper();
    bstr.ToUpper();
    if (bstr == bstrGUID)
    {
        // The GUIDs match, so this device is an instance of MSTape.
        // Get the device description and add it to the list. 
        CComVariant varDescription;
        hr = pPropBag->Read(OLESTR("Description"), &varDescription, NULL);
        if (SUCCEEDED(hr))
        {
            m_DeviceList.AddItem(OLE2T(varDescription.bstrVal),
                pMoniker.Detach());
        }
    }
}

Create the DirectShow Capture Filter

To create the capture filter from the device moniker, call IMoniker::BindToObject with the IID of the filter interface, IBaseFilter. Then call IGraphBuilder::AddFilter to add the filter to the filter graph.

// Create an instance of the capture filter.
CComPtr<IBaseFilter> pCapFilter;
hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter,
    (void**)&pCapFilter);
// (Error checking is omitted for these examples.)

// Add the filter to the graph.
hr = m_pGraph->AddFilter(pCapFilter, L"Capture");

Connect the MPEG-2 Demultiplexer

The next step is to add the MPEG-2 Demultiplexer filter to the graph and connect it to the capture filter.

CComPtr<IBaseFilter> pDemuxFilter;
hr = AddFilterByCLSID(pGraph, CLSID_MPEG2Demultiplexer, &pDemuxFilter);
hr = ConnectFilters(pGraph, pCapFilter, pDemuxFilter);

The AddFilterByCLSID and ConnectFilters functions used here are helper functions defined in the application.

Connect the PSI Parser Filter

Initially, the MPEG-2 Demultiplexer has no output pins. The application must create the output pins that it needs and assign one or more PID values to each pin. The demux then routes the TS packets to the output pins according to the PID assignments. To get the PSI data, create a pin for PID 0x00 and connect it to the PSI Parser filter, as shown in the following code:

// Define the media type for program specific information (PSI) tables.
AM_MEDIA_TYPE media_type;
ZeroMemory(&media_type, sizeof(AM_MEDIA_TYPE));
media_type.majortype = MEDIATYPE_MPEG2_SECTIONS;

// Query the MPEG-2 demux for the IMpeg2Demultiplexer interface.
CComPtr<IMpeg2Demultiplexer> pDemux;
hr = pDemuxFilter.QueryInterface(&pDemux);

// Create the output pin.
CComPtr<IPin> pPin;
hr = pDemux->CreateOutputPin(&media_type, L"PSI", &pPin);

// Map PID 0 (Program Association Table) to the new pin.
ULONG pid = 0x00; 
CComQIPtr<IMPEG2PIDMap> pMap(pPin);
hr = pMap->MapPID(1, &pid, MEDIA_MPEG2_PSI);

Wait for Program Information

Now run the filter graph. This step is necessary to "prime the pump" with PSI data. When the PSI Parser filter receives the PAT, it looks up the PMT PIDs and automatically maps them to the demultiplexer's output pin, so that the parser also receives the PMT tables.

Whenever the PSI Parser receives new program information, it sends a custom DirectShow event, EC_PROGRAM_CHANGED, to the application. At that point, the application can get the program information and build the rest of the filter graph. The sample application handles DirectShow graph events inside the CMainDialog::OnGraphEvent method:

void CMainDialog::OnGraphEvent(long lEvent, long lParam1, long lParam2)
{
    switch (lEvent)
    {
    case EC_PROGRAM_CHANGED:
        UpdateProgramList();
        break;
    // Handle other graph events (not shown).
    }
}

Get the Program Information

To get the program information, use the IMpeg2PsiParser interface on the PSI Parser filter. This interface contains methods for enumerating the programs, as well as enumerating the streams in each program. The following code gets the PID and the stream type for each stream:

// Find the number of programs.
int NumProgs = 0;
hr = pParser->GetCountOfPrograms(&NumProgs);

// Loop through the programs.
for (int iProgram = 0; iProgram < NumProgs; iProgram++)
{
    // For each program, get the program number.
    WORD ProgNum = 0;
    hr = pParser->GetRecordProgramNumber(iProgram, &ProgNum);

    // Find the number of elementary streams in this program.
    WORD cElemStreams = 0;
    hr = pParser->GetCountOfElementaryStreams(iProgram, &cElemStreams);

    // Loop through the streams.
    for (WORD iStream = 0; iStream < cElemStreams; iStream++)
    {
        // For each stream, get the PID and stream type.
        WORD pid = 0;
        BYTE type = 0;
        hr = pParser->GetRecordElementaryPid(iProgram, iStream, &pid);
        hr = pParser->GetRecordStreamType(iProgram, iStream, &type);
        // Update the user interface (not shown).
    }
}

Configure the Audio and Video Pins

After you have the PIDs and stream types for the program, you can finish building the filter graph. Stop the filter graph and create new pins on the demux. These pins will deliver the elementary streams to the decoders. At this point, you may need some specific information about your device and your MPEG-2 decoder. There are two issues: identifying the stream type, and setting the format for the decoder.

Stream Type

The stream type is an 8-bit field in the PMT that identifies the type of content carried in the elementary stream. The difficulty is that various standards define possible stream types. ISO/IEC 13818-1 (MPEG-2 Systems) defines stream types in the range 0x00 to 0x09, and reserves the range 0x0A to 0x7F, but anything from 0x80 to 0xFF is "user private," meaning these values can be defined by other standards bodies. For example, ATSC defines 0x81 as Dolby AC-3 audio.

Table 1 lists the ISO/IEC 13818-1 stream types that are recognized by the sample application.

Table 1. Stream types

Stream type Description
0x01 MPEG-1 video
0x02 MPEG-2 video
0x03 MPEG-1 audio
0x04 MPEG-2 audio

If your device delivers some other stream type, you will need to modify the DVHSGraph::RenderStream method in the file Dvhsgraph.cpp.

Format

When you create a new output pin on the MPEG-2 Demultiplexer, one of the parameters to the IMpeg2Demultiplexer::CreateOutputPin method is a media type. The demux does not validate this media type, but simply presents it to the decoder when the pin connects. What information should go into the media type? In theory, the major type and subtype should be sufficient. All of the other information needed for decoding is already contained in the MPEG byte stream. In practice, however, many MPEG-2 decoders will not connect unless the media type includes a complete format block.

The sample application provides two functions that fill in typical format information for MPEG-1 audio and MPEG-2 video, which are the stream types my D-VHS recorder delivers. These functions were sufficient for the particular decoders that I used to test the application; they may require some modification for other decoders. The PSI Parser sample contains code for various other stream types; see the CProgramProperties::CreateAndRenderAvOutPins method in Progprop.cpp. Here is the code that renders the streams in the sample application:

HRESULT DVHSGraph::RenderStream(PID pid, StreamType type)
{
    HRESULT hr = E_FAIL;
    AM_MEDIA_TYPE mt;
    ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
    switch (type)
    {
    case 0x01:  // MPEG-1 Video
        break;

    case 0x02:  // MPEG-2 Video
        InitMPEG2VideoFormat(mt, SeqHdr, sizeof(SeqHdr));
        hr = RenderVideo(pid, mt);
        break;

    case 0x03:  // MPEG-1 Audio
        InitMPEG1AudioFormat(mt);
        hr = RenderAudio(pid, mt);
        break;

    case 0x04:  // MPEG-2 Audio
        break;
    }

    FreeMediaType(mt);
    return hr;
}

The InitMPEG1AudioFormat and InitMPEG2VideoFormat functions are declared in the file dvhsgraph.cpp. The DVHSGraph::RenderVideo method creates an output pin for video on the demux, if one hasn't already been created. Then it maps the desired PID to this pin and connects the pin to the Video Mixing Renderer filter. The DVHSGraph::RenderAudio method performs the equivalent steps for audio.

Conclusion

This article focused on the programming tasks that are specific to MPEG-2 capture devices:

  • Discovering the device
  • Retrieving program information
  • Mapping elementary stream PIDs
  • Configuring and connecting the decoders

As was noted, some of these steps can vary depending on the type of device and the particular decoders on your system. In addition, there are some other functions that any full-featured capture application will need to provide, such as:

  • Responding to device removal messages
  • Providing VTR transport controls (stop, start, rewind, fast forward)
  • Reading time code from the device
  • Displaying the current device state

These features are implemented in the sample application, and are documented in the DirectShow SDK.

For More Information

This product contains graphics filter software; this software is based in part on the work of the Independent JPEG Group.