Several years ago, I wrote an article for MSDN on programming against the DVR-MS file format.  I'm very pleased with how well the article was received, and to this day I get a lot of people writing to me about the article.  Unfortunately, recently the article was removed from MSDN.  Rather than spending a lot of energy to get it back up on MSDN, I'm including the article here in a blog post (note that this is based on the original Word doc I sent to MSDN to publish; I seem to remember tweaking it a bit with their version after that time, so unfortunately if I did, I don't have those changes).  In "spare time", I've been working on a new version of the article and code based on Windows Vista, but I have no timeline currently in mind for finishing it.

Without further ado...

 

Fun with DVR-MS

Stephen Toub
Microsoft Corporation
February 11, 2005

A few years ago I owned a TiVo. In all honesty, I probably still do, with it buried somewhere in the depths of my apartment’s closets, I’m sure thoroughly littered with dust by now. Occupying the vacated and prized location next to my television is an even prettier and more sophisticated marvel of modern software and electronics, a Microsoft Windows XP Media Center 2005. “Mickey,” as my family has aptly named and personified the device, has a slew of fantastic features. However, whenever my tech-savvy friends ask me to choose and name one reason I’d recommend switching to this platform from whichever digital video recorder (DVR) they currently use, my answer is simple: file access to the recorded television shows.

DVR-MS files are created by the Stream Buffer Engine (SBE) introduced in Windows XP Service Pack 1, and are used by Media Center for storing recorded television. In this article, I’ll demonstrate how you can use DirectShow to work with and manipulate DVR-MS files. In the process, I’ll show you some useful utilities I’ve created for processing DVR-MS files, and will provide you with the tools and libraries you’ll need to write your own. So, open Visual Studio .NET, grab some popcorn, and enjoy the ride.

NOTE: This article assumes that you have a working MPEG2 decoder in your system. Also, some DVR-MS files are copy protected due to policy set by the content owner or broadcaster. This protection is determined when the file is generated by examining the broadcaster's copy protection flag (CGMS-A) and will limit how and when you’re able to access a particular DVR-MS file. For example, movies recorded on a premium station like HBO may be encrypted, and consequently the techniques described in this article will not be applicable.

Playing DVR-MS Files

When it comes to video files, playing them is probably the most important action that can be performed, so that’s where I’ll start our journey. There are a variety of ways to play DVR-MS files from within your own applications, and I’ll demonstrate a few of them here. To do so, I’ve created the simple application shown in Figure 1, available in the code download associated with this article.

 1. PlayingDvrMs

Figure 1. Sample application to play DVR-MS files.

The first and simplest way to play a DVR-MS file is to use the System.Diagnostics.Process class to execute it. Since Process.Start wraps the unmanaged ShellExecuteEx function from shell32.dll, this takes advantage of the same ability to play a DVR-MS file as is used when double-clicking on one from Windows Explorer:

private void btnProcessStart_Click(object sender, System.EventArgs e)
{
    Process.Start(txtDvrmsPath.Text);
}

This also means that the video will be played in a separate process running in whatever is your default handler for DVR-MS files; on most machines, and on mine, this will be Windows Media Player (I use Windows Media Player 10, and if you don’t, I’d suggest you upgrade to it for free at http://www.microsoft.com/windows/windowsmedia/mp10/default.aspx). Of course, another overload of Process.Start that accepts both an executable path as well as arguments can be used to launch the DVR-MS file in whatever player you desire, regardless of whether it’s the default handler for the .dvr-ms extension:

private void btnProcessStart_Click(object sender, System.EventArgs e)
{
    Process.Start(
        @"c:\Program Files\Windows Media Player\wmplayer.exe", "\"" + txtDvrmsPath.Text + "\"");
}

You should note that it is necessary when doing this to surround the path to the DVR-MS file (as supplied here by the contents of the TextBox named txtDvrmsPath) in quotes, since the contents is being used as a command-line argument to wmplayer.exe. Otherwise, any spaces in the path would result in the path being split and interpreted as multiple arguments.

Process.Start returns a Process instance that represents the started process, which means you can take advantage of the functionality provided by Process to further interact with Windows Media Player. For example, you might want to wait for the video to stop before allowing the user to continue in your application, a task which can be accomplished using the Process.WaitForExit method:

private void btnProcessStart_Click(object sender, System.EventArgs e)
{
    using(Process p = Process.Start(txtDvrmsPath.Text))
    {
        p.WaitForExit();
    }
}

Of course, this only waits for Media Player to shutdown, as your app has no real view into what Media Player is doing, other than that you initially requested that it play the file you specified. Coding it as above will also freeze the GUI of your application while Media Player is open, a problem which could be solved by subscribing to the Process’s Exited event rather than blocking with the WaitForExit method.

All in all, this solution is simple and easy to code, but it is very inflexible and plays the video externally to your application. It’s probably only appropriate for situations in which you want to allow a user to view a specified file, but in contexts where your application doesn’t care what the video is and where the application doesn’t interact with the video at all. For example, this could be appropriate if your application is a download agent and you want to allow users to view the video files that have been copied locally.

Since we know that Windows Media Player can play DVR-MS files, a better solution for most scenarios is to host an instance of the Windows Media Player ActiveX control in your application. In Visual Studio .NET, simply right click in the Toolbox, choose to add a control, and select the Windows Media Player COM control. It’ll then be available in your toolbox, as shown in Figure 2.

 2. WMPControlInToolbox

Figure 2. Windows Media Player ActiveX control in the Toolbox.

With an instance of the ActiveX control on your form, getting it to play a DVR-MS file is as simple as setting the player’s URL property:

player.URL = txtDvrmsPath.Text;

In my sample application, I’ve chosen to take it a step further. I’ve created a System.Windows.Forms.Panel that lives on the form where I want the video to show up. When a user requests that the selected video is played using Media Player, I create a new instance of the Media Player control, add it to the Panel’s children control collection, dock it to full size, and set its URL property. This scheme allows me to fully control the lifetime of Media Player yet still easily administer its placement on the form (it also makes it easy to demonstrate other methods for playing video, as you’ll see in a moment) without having to worry about absolute positioning values. A screenshot of this in action is shown in Figure 3, and the code I use is shown here:

private void btnWmp_Click(object sender, System.EventArgs e)
{
    AxWindowsMediaPlayer player = new AxWindowsMediaPlayer();
    pnlVideo.Controls.Add(player);
    player.Dock = DockStyle.Fill;
    player.PlayStateChange +=
        new _WMPOCXEvents_PlayStateChangeEventHandler(player_PlayStateChange);
    player.URL = txtDvrmsPath.Text;
}

private void player_PlayStateChange(object sender, _WMPOCXEvents_PlayStateChangeEvent e)
{
    AxWindowsMediaPlayer player = (AxWindowsMediaPlayer)sender;
    if (e.newState == (int)WMPLib.WMPPlayState.wmppsMediaEnded ||
        e.newState == (int)WMPLib.WMPPlayState.wmppsStopped)
    {
        player.Parent = null; // removes the control from the panel
        ThreadPool.QueueUserWorkItem(new WaitCallback(CleanupVideo), sender);
    }
}

private void CleanupVideo(object video)
{
    ((IDisposable)video).Dispose();
}

3. PlayingDvrMs_WMPEmbedded

Figure 3. Embedded DVR-MS playback with the WMP control.

To prevent the Media Player toolbar from being shown, you can change the control’s uiMode property:

player.uiMode = "none";

and to prevent the display of the Media Player context menu when a user right-clicks on the control, you can set its enableContextMenu property to false:

player.enableContextMenu = false;

You’ll notice that just before playing the DVR-MS file, I register an event handler with the player’s PlayStateChange event. This allows me to remove the player from the Panel when playback has stopped. In the handler for the PlayStateChange event, I check whether playback has ended, and in that case, I remove the player from its parent control (the panel) and queue a work item to the .NET ThreadPool. This work item simply disposes of the player control. I do this disposal in a background thread because I can’t do it directly within the PlayStateChange event handler. Disposing of the control within this event handler will cause an exception to be thrown within the control itself, since the event handler was raised from within the control and more processing will be necessary by the control after executing my handler. Disposing it in the handler will cause that functionality to break, and so I give it the time required by delaying the action slightly until after the event handler has finished. You’ll see that this same technique is required when using the next demonstrated playback mechanism.

Hosting the Windows Media Player ActiveX control has a lot of upside. It’s very easy to use and provides a wealth of functionality. However, Windows Media Player utilizes DirectX, specifically DirectShow, to play DVR-MS files (later in this article I’ll discuss DirectShow in much more detail). Rather than relying on Windows Media Player’s interactions with DirectX, you can use Managed DirectX from your application and bypass Windows Media Player altogether.

The most recent version of Managed DirectX at the time of this writing is part of the DirectX 9.0 SDK Update February 2005, available at http://www.microsoft.com/downloads/details.aspx?FamilyId=77960733-06E9-47BA-914A-844575031B81 (for material covered later in this article, you’ll also need the February 2005 Extras download, available at http://www.microsoft.com/downloads/details.aspx?FamilyId=8AF0AFA9-1383-44B4-BC8B-7D6315212323). This SDK installs the AudioVideoPlayback.dll assembly into your Global Assembly Cache (GAC), making it available to your application (the DirectX runtime installation also installs this DLL so that your end-users will have access to it). AudioVideoPlayback is a high-level wrapper around a minimal amount of DirectShow functionality that allows you to play video and audio files within your .NET applications.

As with the Windows Media Player ActiveX control, using AudioVideoPlayback is very straightforward.

private void btnManagedDirectX_Click(object sender, System.EventArgs e)
{
    Video v = new Video(txtDvrmsPath.Text);
    Size s = pnlVideo.Size;
    v.Owner = pnlVideo;
    v.Ending += new EventHandler(v_Ending);
    v.Play();
    pnlVideo.Size = s;
}

private void v_Ending(object sender, EventArgs e)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(CleanupVideo), sender);
}

private void CleanupVideo(object video)
{
    ((IDisposable)video).Dispose();
}

A new Microsoft.DirectX.AudioVideoPlayback.Video object is instantiated and is supplied the path to the DVR-MS file to be played. When a Video is played, it automatically resizes itself (more specifically, its owner control) to the appropriate size for the video being played; to counteract this, I store the original size of the parent panel control so that I can reset its size after playback begins. Just as with the ActiveX control, I register an event handler to fire when playback stops, and then play the video. When playback ends, just as with the ActiveX control (and for the same reasons), I queue a work item to the ThreadPool that will dispose the Video object. It’s very important to dispose the Video object when you’re done using it; otherwise, a significant amount of unmanaged resources could be used needlessly, and since this object has a very small managed footprint, the garbage collector (GC) won’t have a significant incentive to run a collection anytime soon, thereby leaving these unmanaged resources allocated indefinitely, unless you do it manually with IDisposable. The screenshot in Figure 4 demonstrates the usage of the AudioVideoPlayback functionality.

4. PlayingDvrMs_ManagedDirectX

Figure 4. Embedded playback with AudioVideoPlayback.

Of course, AudioVideoPlayback is a high-level wrapper around DirectShow, but there’s no reason you can’t create your own managed wrapper (in fact, we’ll do just that later in this article). The simplest way to do so is to take advantage of tlbimp.exe (or, similarly, the COM type library import capabilities of Visual Studio .NET. Both Visual Studio .NET and tlbimp.exe rely on the same libraries in the Framework to perform the import).

The core library for the DirectShow runtime is quartz.dll, located at %windir%\system32\quartz.dll. It contains the most important COM interfaces and coclasses for audio and video playback, which we’ll be examining in much more detail later in the article. Running tlbimp.exe on quartz.dll produces an interop library Interop.QuartzTypeLib.dll (the description of this assembly will be “ActiveMovie control type library,” as a former incarnation of DirectShow was named ActiveMovie), exposing FilgraphManagerClass (filter graph manager) and the IVideoWindow interface. To play a video, you simply create a new instance of the graph manager and use the RenderFile method, passing in the path to your DVR-MS file, in order to initialize the object for playback. You can then use the IVideoWindow interface, implemented by FilgraphManagerClass, to control playback options such as the owner window, the position of the video on the parent, and the caption of the video window. To start playback, the Run method is used. The WaitForCompletion method can be used to wait for the video to stop playback (alternatively, a positive number of milliseconds can be specified as the maximum amount of time to wait), and the Stop method can be used to halt playback. To destroy the object and release all unmanaged resources used for playback, including the playback window itself, the System.Runtime.InteropServices.Marshal class and its ReleaseComObject method come in handy. A screenshot of using quartz.dll is shown in Figure 5.

private void btnQuartz_Click(object sender, System.EventArgs e)
{
    FilgraphManagerClass fm = new FilgraphManagerClass();
    fm.RenderFile(txtDvrmsPath.Text);
    IVideoWindow vid = (IVideoWindow)fm;
    vid.Owner = pnlVideo.Handle.ToInt32();
    vid.Caption = string.Empty;
    vid.SetWindowPosition(0, 0, pnlVideo.Width, pnlVideo.Height);
    ThreadPool.QueueUserWorkItem(new WaitCallback(RunQuartz), fm);
}

private void RunQuartz(object state)
{
    FilgraphManagerClass fm = (FilgraphManagerClass)state;
    fm.Run();
    int code;
    fm.WaitForCompletion(Timeout.Infinite, out code);
    fm.Stop();
    while(Marshal.ReleaseComObject(fm) > 0);
}

5. PlayingDvrMs_Quartz

Figure 5. Embedded playback using quartz.dll.

I’ve just shown you a few ways in which you can play DVR-MS files from within your own applications. While I’ve discussed multiple methods for doing so (and my list isn’t exhaustive), all of these methods rely on DirectShow for playback functionality. As such, a brief introduction to DirectShow (or a refresher for those of you with DirectShow experience) is in order.

Introduction to DirectShow and GraphEdit

At its core, an application that uses DirectShow to work with video files does so through a set of components called filters. A filter usually performs a single operation on a stream of multimedia data. A huge number of filters exist, each of which performs a different task, for example reading in a DVR-MS file, writing out an AVI file, decoding an MPEG-2 compressed video, rendering video and audio to the video card and sound card, and so on. Instances of these filters can be connected and combined into a graph of filters, which is then managed by the DirectShow Filter Graph Manager component (you saw this briefly when I previously mentioned quartz.dll). These graphs are directed and acyclic, meaning that a particular connection between two filters only allows data to flow in one direction and that the data can only flow through a particular filter once. This flow of data is referred to as a stream, and filters are said to process these streams. Filters are connected to other filters through pins they expose, such that an output pin on one filter is connected to an input pin on another filter, sending a data stream emanating from the former into the latter.

To demonstrate this, and to show the graphs being used throughout this article, I’ll take advantage of a utility included in the DirectX SDK called GraphEdit. GraphEdit can be used to visualize filter graphs, a feature which comes in very handy when determining how to build up graphs for particular purposes as well as when debugging the graphs you’re building. Later on, I’ll show you how GraphEdit can be used to connect to and visualize filter graphs running in your own applications.

For now, run GraphEdit. Under the file menu, select Render Media File… and choose any valid DVR-MS file available to you locally (note that you’ll probably need to change the extension filter in the open file dialog to All Files rather than All Media Files, since the last distributed version of GraphEdit doesn’t categorize the .dvr-ms extension as a media file). You should see a graph appear that looks similar to the one shown in Figure 6.

6. GraphEdit_RenderedDvrMs

Figure 6. GraphEdit ready to play a DVR-MS file.

At this point, GraphEdit has constructed a filter graph capable of playing the selected DVR-MS file. Each one of those blue boxes is a filter, and the arrows show how the input and output pins on each filter are connected to each other to form the graph. The first filter in the graph is an instance of the StreamBufferSource filter, as exposed by the %windir%\system32\sbe.dll library on Windows XP SP1 and later. This filter was chosen because it’s configured in the registry as the source filter for the .dvr-ms extension (HKCL\Media Type\Extensions\.dvr-ms\Source Filter). It’s used to read in a file from disk and to send that file’s data out to the rest of the graph in streams. It provides three streams from a DVR-MS file.

The first is the audio stream. If you examine the pin properties for the first pin (you can access pin properties by right-clicking on the pin in GraphEdit), DVR Out – 1, you’ll see that the pin’s major type is Audio and that its subtype is Encrypted/Tagged, which means before we can do anything with this data it must first be decrypted and/or detagged. This process is handled by the Decrypter/Detagger filter, exposed from %windir%\system32\encdec.dll. Decrypter/Detagger takes as input the encrypted/tagged audio stream and in turn sends out an MPEG-1 audio stream (or a dolby-AC3 stream for high-definition content), a fact you can verify by examining the In(Enc/Tag) and Out pins on that filter. From there, the audio is sent to the MPEG Audio Decoder filter, exposed from quartz.dll, where the audio is decompressed into a Pulse Code Modulation (PCM) audio stream. The final filter for the audio stream, a DirectSound Audio Renderer (also exposed from quartz.dll), takes in this PCM audio data and plays it on the computer’s sound card.

The second stream provided by the DVR-MS source filter contains the closed captioning data for the recorded television show. As with the audio stream, the closed captioning stream is encrypted/tagged, so it must first pass through a Decrypter/Detagger filter. If you look at the Out pin on this filter, you’ll see that its major type is AUXLine21Data and that its subtype is Line21_BytePair. Closed captioning in television shows is sent as part of the television image, specifically encoded into line 21 of the image.

The third stream emanating from the DVR-MS source filter is the video feed. As with the audio and closed captioning data, this stream is encrypted/tagged, so it must first pass through a Decrypter/Detagger filter. The output of the Decrypter/Detagger filter is an MPEG-2 video stream, so it must then pass through an MPEG-2 video decoder before the video can be rendered. Microsoft does not ship an MPEG-2 decoder with Windows, so a 3rd party decoder must be available on the system for playback to be possible. The decoded video stream is then sent to the default video renderer, exposed from quartz.dll.

Clicking the green play button above the graph will cause a new window entitled ActiveMovie Window to appear and the DVR-MS file to play within that window. Note that since the closed captioning Decrypt/Tag Out pin isn’t connected to anything, the closed captioning data isn’t used when rendering the video. You can change this by modifying the graph. As practice, first delete the default video renderer (click on the filter and type the Delete key), as this renderer is not capable of handling multiple inputs. Specifically, we need a renderer that can display the video stream and overlay on top of it bitmaps containing the rendered closed captioning data. How do you get the line 21 byte pairs from the Decrypter/Detagger filter to be rendered as bitmaps? Windows actually ships with a DirectShow filter for just this task. Using the Insert Filters… command under the Graph menu, expand the DirectShow filters node in the tree view and select the Video Mixing Renderer 9 filter. Click the insert button to add an instance of this filter to the graph, and then close the insert filters dialog. A Video Mixing Renderer 9 filter is now part of the graph, but is not connected to anything and won’t be used (in fact, if you hit the play button now, since the video stream is not connected to a renderer, only the audio will be played). Click and drag the Video Output pin on the MPEG-2 decoder to the VMR Input0 pin on the renderer (note that if you’re using a decoder other than NVDVD, the name of the video output pin may differ, but the concept will be the same). If you were to play the graph now, you’d see output almost identical to when using the default video renderer. However, you’ll notice now that the renderer filter exposes multiple input pins (filters can actually dynamically change what pins they expose based on what other filters they’re connected to). We can take advantage of this by connecting the closed captioning Decrypter/Detagger filter’s Out pin to the VMR Input1 pin on the renderer. Automatically, GraphEdit inserts a Line 21 Decoder 2 filter, connecting the Decrypter/Detagger filter to the decoder filter and the decoder filter to the renderer filter. You should now see a graph similar to that shown in Figure 7. When you play this graph, you’ll see the closed captioning appear as text over the video as you’d expect.

7. GraphEdit_Line21Decoder

Figure 7. Incorporating closed captions into video display.

At this point, those of you who are unfamiliar with DirectShow may be wondering how the Line 21 Decoder 2 filter was found, and for that matter how the whole graph was constructed in the first place simply by using GraphEdit’s Render Media File operation. GraphEdit relies on functionality provided by the IGraphBuilder interface to find and select the appropriate filters and to connect them to each other as necessary (the FilgraphManager component we looked at briefly while examining how to play DVR-MS files implements IGraphBuilder, and in fact the RenderFile method we used is part of the IGraphBuilder interface).

The mechanism used to automatically build filter graphs is known as Intelligent Connect. Since you don’t really need to know the specifics of Intelligent Connect unless you are implementing your own filters and want to make them available for automatic graph building, I won’t cover the subject in depth here, and instead I refer you to the detailed documentation on the subject that’s included in the DirectX SDK. In a nutshell, however, the RenderFile method is a simple wrapper around two other methods on IGraphBuilder: AddSourceFilter and Render. RenderFile first calls AddSourceFilter, which for local files simply looks up in the registry the type of source filter necessary for the extension of the file being played, adds the appropriate filter instance to the filter graph, and configures it to point at the specified source file. For each output pin on this source filter, RenderFile then calls the Render method, which attempts to find a path from this pin to a renderer in the graph. If the pin implements the IStreamBuilder interface, Render just delegates to that implementation, leaving all of the details up to the filter’s implementation. Otherwise, Render attempts to find a filter to which this pin can connect. To do so, it looks for cached filters that it might have cached earlier in the graph building process, for any filters already part of the graph that have unconnected input pins, and in the registry for compatible filter types using the IFilterMapper interface. If a filter can be found, it then repeats this process for this new filter until a renderering filter is reached, at which point it stops successfully. If one can’t be found, Intelligent Connect will be unsuccessful in building a graph. This points to one of the downsides of relying on Intelligent Connect: it doesn't always work. Additionally, if new filters are installed onto your machine, Intelligent Connect may choose those new filters instead of the ones that you are currently expecting in your applications. As such, you may choose to avoid it in your designs (as I’ll demonstrate later, if you know exactly what filters you want in your graph, it’s easy to build the graph explicitly without Intelligent Connect).

Now that you have a sense for DirectShow, we’ll be using it programmatically to do lots of cool manipulations with DVR-MS files. After all, once the DVR-MS source filter is loaded into a graph, we can work with the data coming out of the DVR-MS like we would any other streams of audio and video data, manipulating them in an unlimited number of ways.

DirectShow Interfaces

First things first, however, we need to be able to work with DirectShow programmatically. From unmanaged code, this is possible out-of-the-box, as the SDK includes all of the header files necessary to access the DirectShow libraries from C++. From managed code, things are a bit trickier. While Managed DirectX does include the AudioVideoPlayback.dll library discussed earlier, the library is very high level, providing abstractions at the Video and Audio level, whereas we need to be able to manipulate filter graphs at the filter and pin level. For at least the current version, Managed DirectX doesn’t help us, though I imagine this will improve in the future.

What about quartz.dll? The type library for quartz.dll exposes some of the functionality we need, with a full list of the interfaces exposed listed here:

Interface Description
IAMCollection A collection of filter graph objects, such as filters or pins.
IAMStats Enables an application to retrieve performance data from the graph manager. Filters can use this interface to record performance data.
IBasicAudio Enables applications to control the volume and balance of the audio stream.
IBasicVideo Enables applications to set video properties such as the destination and source rectangles
IBasicVideo2 Derives from the IBasicVideo interface and provides an additional method for applications to retrieve the preferred aspect ratio of the video stream.
IDeferredCommand Enables an application to cancel or modify graph-control commands that the application previously queued using the IQueueCommand interface.
IFilterInfo Manages information about a filter and provides access to the filter and to the IPinInfo interfaces representing the pins on the filter.
IMediaControl Provides methods for controlling the flow of data through the filter graph. It includes methods for running, pausing, and stopping the graph.
IMediaEvent Contains methods for retrieving event notifications and for overriding the Filter Graph Manager's default handling of events.
IMediaEventEx Derives from IMediaEvent and adds methods that enable an application window to receive messages when events occur.
IMediaPosition Contains methods for seeking to a position within a stream.
IMediaTypeInfo Contains methods for retrieving the media type of a pin connection.
IPinInfo Contains methods for retrieving information about a pin, and for connecting pins.
IQueueCommand Enables an application to queue graph-control commands in advance.
IRegFilterInfo Provides access to filters in the Windows registry, and adds registered filters to the filter graph.
IVideoWindow Contains methods to set the window owner, the position and dimensions of the window, and other window properties.

This is certainly a great start, but it doesn’t provide us with some of the most crucial interfaces for dealing with graphs and filters. For example, the IGraphBuilder interface, which is one of the more commonly used interfaces for constructing graphs manually, is not included. Nor is the IBaseFilter interface, which represents a particular filter instance and provides for access to its pins. The following table lists the main interfaces I want access to for what I want to accomplish with graphs in this article:

Interface Description
IBaseFilter Provides methods for controlling a filter. Applications can use this interface to enumerate pins and query for filter information.
IConfigAsfWriter2 Provides methods for getting and setting the Advanced Streaming Format (ASF) profiles the WM ASF Writer filter will use to write files as well as supporting new capabilities in the Windows Media Format 9 Series SDK such as two-pass encoding and support for deinterlaced video.
IFileSinkFilter Implemented on filters that write media streams to a file.
IFileSourceFilter Implemented on filters that read media streams from a file.
IGraphBuilder Provides methods that enable an application to build a filter graph.
IMediaControl Provides methods for controlling the flow of data through the filter graph. It includes methods for running, pausing, and stopping the graph.
IMediaEvent Contains methods for retrieving event notifications and for overriding the Filter Graph Manager's default handling of events.
IMediaSeeking Contains methods for querying for the current position and for seeking to a specific position within a stream.
IWmProfileManager Used to create profiles, load existing profiles, and save profiles.

Additionally, there are a variety of COM classes I need to instantiate explicitly, the most important of which are shown below along with their class IDs and a description of each class:

Class Class ID Description
Filter Graph Manager e436ebb3-524f-11ce-9f53-0020af0ba770 Builds and controls filter graphs. This object is the central component in DirectShow.
Decrypter/Detagger Filter C4C4C4F2-0049-4E2B-98FB-9537F6CE516D Conditionally decrypts samples that are encrypted by the Encrypter/Tagger filter. The output type matches the original input type received by the Encrypter/Tagger filter.
WM ASF Writer Filter 7c23220e-55bb-11d3-8b16-00c04fb6bd3d Accepts a variable number of input streams and creates an Advanced Streaming Format (ASF) file.

As pointed out by Eric Gunnerson in his blog entry about DirectShow and C# at http://blogs.msdn.com/ericgu/archive/2004/09/20/232027.aspx, one quick and easy way to import interfaces is by using the DirectShow Interface Definition Language (IDL) files that come with the DirectX SDK. These files contain the COM interface definitions for most of the interfaces in which I’m interested. I can create my own IDL file that is authored to produce a type library, and then by run it through the Microsoft® Interface Definition Language (MIDL) compiler (midl.exe). This produces a type library, which can then be converted into a managed assembly using the .NET Framework tool Type Library Importer (tlbimp.exe).

Unfortunately, as Eric also points out, it’s not a perfect solution. First, not all of the interfaces I need are described in the IDL files that come with the DirectX SDK, such as IMediaEvent and IMediaControl. Second, even if they were, often more control is needed over the creation of the interop signatures than is provided by tlbimp.exe. For example, IMediaEvent.WaitForCompletion (which we’ll examine later in this article) returns an E_ABORT HRESULT if the time specified by the user expires before the graph was run to completion; this translates into an exception being thrown in .NET, which is not appropriate if you’re calling WaitForCompletion frequently in a polling loop (which I plan to do). Additionally, there isn’t a one-to-one mapping between IDL types and managed types; in fact, there are scenarios where a type might be marshaled differently based on the context in which it will be used. For example, in the axcore.idl file in the DirectX SDK, the IEnumPins interface exposes the following method:

HRESULT Next(
    [in] ULONG cPins, // Retrieve this many pins.
    [out, size_is(cPins)] IPin ** ppPins, // Put them in this array.
    [out] ULONG * pcFetched // How many were returned?
);

When this is compiled into a type library and converted by tlbimp.exe, the resulting assembly contains the following method:

void Next(
    [In] uint cPins,
    [Out, MarshalAs(UnmanagedType.Interface)] out IPin ppPins,
    [Out] out uint pcFetched
);

Whereas the unmanaged IEnumPins::Next could be called with any positive integer value for cPins, it would be erroneous to call the managed version with a value for cPins other than 1, since rather than ppPins being an array of IPin instances, it’s a reference to a single IPin instance.

For all of these reasons, along with the relative simplicity of the DirectShow interfaces, I opted to implement the COM interface interop definitions by hand in C#; this requires more work, but it allows for the best control over exactly what’s marshaled, how, and when (note, however, that a good starting point when creating these hand coded interop definitions is the MSIL generated by tlbimp.exe, or even better, a decompiled C# implementation of these imported type libraries as can be generated using Lutz Roeder’s .NET Reflector, available at http://www.aisto.com/roeder/dotnet/). In the code download associated with this article, you’ll find hand-coded C# interfaces for each of the unmanaged DirectShow interfaces I make use of in this article. As an example, here’s a C# implementation of the IGraphBuilder interface previously discussed:

[ComImport]
[Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IGraphBuilder
{
    void AddFilter([In] IBaseFilter pFilter,
        [In, MarshalAs(UnmanagedType.LPWStr)] string pName);
    void RemoveFilter([In] IBaseFilter pFilter);
    IEnumFilters EnumFilters();
    IBaseFilter FindFilterByName([In, MarshalAs(UnmanagedType.LPWStr)] string pName);
    void ConnectDirect([In] IPin ppinOut, [In] IPin ppinIn,
        [In, MarshalAs(UnmanagedType.LPStruct)] AmMediaType pmt);
    void Reconnect([In] IPin ppin);
    void Disconnect([In] IPin ppin);
    void SetDefaultSyncSource();
    void Connect([In] IPin ppinOut, [In] IPin ppinIn);
    void Render([In] IPin ppinOut);
    void RenderFile(
        [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFile,
        [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrPlayList);
    IBaseFilter AddSourceFilter(
        [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFileName,
        [In, MarshalAs(UnmanagedType.LPWStr)] string lpwstrFilterName);
    void SetLogFile(IntPtr hFile);
    void Abort();
    void ShouldOperationContinue();
}

An instance of the filter graph manager component can then be cast to and used through my IGraphBuilder interface. So how do I get an instance of the filter graph manager component? I use code like the following:

public class ClassId
{
    public static readonly Guid FilterGraph =
        new Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770");
    public static readonly Guid WMAsfWriter =
        new Guid("7C23220E-55BB-11D3-8B16-00C04FB6BD3D");
    public static readonly Guid DecryptTag =
        new Guid("C4C4C4F2-0049-4E2B-98FB-9537F6CE516D");
    ...
    public static object CoCreateInstance(Guid id)
    {
        return Activator.CreateInstance(Type.GetTypeFromCLSID(id));
    }
}

With this wrapper in place, I can then use create an instance of the filter graph manager, configure a filter graph capable of playing a DVR-MS file, and play the file, all in a total of five lines of code:

object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);
((IGraphBuilder)filterGraph).RenderFile(pathToDvrmsFile);
((IMediaControl)filterGraph).Run();
EventCode status;
((IMediaEvent)filterGraph).WaitForCompletion(Timeout.Infinite, out status);

Now that we know how to work with DirectShow from managed code, let’s see how to take advantage of this to do some very cool things.

Transcoding to WMV

If Internet search engines provide any clues, one of the most popular things people want to do with DVR-MS files is convert them to Windows Media Video files. This is an easy task to accomplish with the framework we’ve created thus far for working with DVR-MS files and DirectShow. Simply put, all I need to do is create a graph that uses a DVR-MS source filter and a WM ASF Writer filter sink (which encodes and writes out WMV files), with the appropriate filters and connections between them. I’m purposely being vague about those intermediate filters because I can let Intelligent Connect find and insert them for me. As an example of how easy this is to do by hand, follow these simple steps to create the appropriate conversion graph in GraphEdit:

1. Open GraphEdit.

2. Select Insert Filters from the Graph menu and insert a DirectShow WM ASF Writer filter. When prompted for an output file name, enter the name of the target file with the .wmv extension.

3. From the File menu, choose Render Media File, and in the resulting open file dialog, select the input DVR-MS file (again, you’ll most likely need to change the file extension filter to be All Files rather than All Media Files).

GraphEdit will use the graph’s RenderFile method to add a source filter for the DVR-MS file and connect it to the appropriate renderers through whatever series of intermediate filters are necessary. Since the WM ASF Writer filter sink is already in the graph when this takes place, RenderFile using Intelligent Connect will route the streams to it rather than inserting a new default renderer filter. You should see a graph similar to that in Figure 8.

8. GraphEdit_WmvConversion

Figure 8. Graph for transcoding DVR-MS to WMV.

Doing this programmatically is very straightforward, and can be accomplished with the following code:

// Get the filter graph
object filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);
DisposalCleanup.Add(filterGraph);
IGraphBuilder graph = (IGraphBuilder)filterGraph;

// Add the ASF writer and set the output name
IBaseFilter asfWriterFilter = (IBaseFilter)
ClassId.CoCreateInstance(ClassId.WMAsfWriter);
DisposalCleanup.Add(asfWriterFilter);
graph.AddFilter(asfWriterFilter, null);
IFileSinkFilter sinkFilter = (IFileSinkFilter)asfWriterFilter;
sinkFilter.SetFileName(OutputFilePath, null);

// Render the DVR-MS file and run the graph
graph.RenderFile(InputFilePath, null);
RunGraph(graph, asfWriterFilter);

A filter graph is created, the WM ASF Writer filter added to it and configured to point to the appropriate output file path, and the DVR-MS file is then added to the graph and rendered using the graph’s RenderFile method. Unfortunately, this doesn’t provide for much flexibility in terms of controlling how the WMV file is encoded. To do that, we need to configure the WM ASF Writer with a profile, which can be done by inserting the following code before the call to RenderFile:

// Set the profile to be used for conversion
if (_profilePath != null)
{
    // Load the profile XML contents
    string profileData;
    using(StreamReader reader = new StreamReader(File.OpenRead(_profilePath)))
    {
        profileData = reader.ReadToEnd();
    }

    // Create an appropriate IWMProfile from the data
    IWMProfileManager profileManager = ProfileManager.CreateInstance();
    DisposalCleanup.Add(profileManager);
    IntPtr wmProfile = profileManager.LoadProfileByData(profileData);
    DisposalCleanup.Add(wmProfile);

    // Set the profile on the writer
    IConfigAsfWriter2 configWriter = (IConfigAsfWriter2)asfWriterFilter;
    configWriter.ConfigureFilterUsingProfile(wmProfile);
}

This snippet assumes that the path to a profile PRX file has been stored in the string member variable _profilePath. First, the XML contents of the profile are read into a string using System.IO.StreamReader. A Windows Media Profile Manager (accessed through a IWMProfileManager interface) is then created, and the profile loaded into it using the manager’s LoadProfileByData method. This provides us with an interface pointer to the loaded profile, which can be used to configure the WM ASF Writer filter. The WM ASF Writer filter implements the IConfigAsfWriter2 interface which provides the ConfigureFilterUsingProfile method that configures the writer based on a profile specified using an interface pointer.

With the graph created and configured, all that remains is to run it, which I accomplish using my aptly named RunGraph method. The method starts by obtaining IMediaControl and IMediaEvent interfaces for the specified graph. It also tries to obtain an IMediaSeeking interface which can be used to track how much of the source DVR-MS file has been processed. The IMediaControl interface is then used to run the graph, from which point on the rest of the code in the method is simply keeping track of how far along the conversion has progressed. Until the graph has finished running, the code continually polls the IMediaEvent.WaitForCompletion method, which will return a status code of EventCode.None (0x0) if the wait time is reached before the graph has run to completion. If that occurs, the IMediaSeeking interface is used to query how much of the DVR-MS file has been processed as well as the duration of the file, allowing me to compute what percentage of the file has been processed.

When the graph eventually completes its run, IMediaEvent.WaitForCompletion returns EventCode.Complete (0x1), and IMediaControl.Stop is used to stop the graph.

protected void RunGraph(IGraphBuilder graphBuilder, IBaseFilter seekableFilter)
{
    IMediaControl mediaControl = (IMediaControl)graphBuilder;
    IMediaEvent mediaEvent = (IMediaEvent)graphBuilder;
    IMediaSeeking mediaSeeking = seekableFilter as IMediaSeeking;

    if (!CanGetPositionAndDuration(mediaSeeking))
    {
        mediaSeeking = graphBuilder as IMediaSeeking;
        if (!CanGetPositionAndDuration(mediaSeeking)) mediaSeeking = null;
    }
    using(new GraphPublisher(graphBuilder, Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf"))
    {
        mediaControl.Run();
        try
        {
            OnProgressChanged(0);
            bool done = false;
            while(!CancellationPending && !done)
            {
                EventCode statusCode = EventCode.None;
                int hr = mediaEvent.WaitForCompletion(PollFrequency, out statusCode);
                switch(statusCode)
                {
                    case EventCode.Complete:
                        done = true;
                        break;
                    case EventCode.None:
                        if (mediaSeeking != null)
                        {
                            ulong curPos = mediaSeeking.GetCurrentPosition();
                            ulong length = mediaSeeking.GetDuration();
                            double progress = curPos * 100.0 / (double)length;
                            if (progress > 0) OnProgressChanged(progress);
                        }
                        break;
                    default:
                        throw new DirectShowException(hr, null);
                }
            }
            OnProgressChanged(100);
        }
        finally { mediaControl.Stop(); }
    }
}

Isn’t that simple? DirectShow is an amazing piece of technology. This code will allow you to convert non-DRM’d, NTSC, SD content stored in DVR-MS files into WMV files. As you’ll see if you examine the files in the code download for this article, I’ve coded this function into a base abstract class called Converter. A derived class, in this case WmvConverter, simply derives from Converter, builds up the appropriate graph, and then calls the base class’ RunGraph method. Converter additionally exposes properties and events that can be used to configure, monitor, and halt the graph’s process, and as you’ll see in the following section, Converter exposes functionality that makes debugging graphs much easier.

Debugging Filter Graphs

You’ll notice in the RunGraph method that the graph is run inside of a using block that looks the following:

using(new GraphPublisher(graphBuilder, Path.GetTempPath()+Guid.NewGuid().ToString("N")+".grf"))
{
    ... // run the graph
}

The GraphPublisher class I’ve used here is a custom class I wrote to help with the debugging of graphs. It serves two purposes. First, if a file path is specified as the second parameter to GraphPublisher’s constructor, it saves the graph represented by the graphBuilder object out to that file (which should use the .grf extension). This file can later be opened by GraphEdit, allowing you to see the entire graph as it existed when it was published. This functionality is made possible through the filter graph manager’s implementation of the IPersistStream interface:

private const ulong STGM_CREATE = 0x00001000L;
private const ulong STGM_TRANSACTED = 0x00010000L;
private const ulong STGM_WRITE = 0x00000001L;
private const ulong STGM_READWRITE = 0x00000002L;
private const ulong STGM_SHARE_EXCLUSIVE = 0x00000010L;

[DllImport("ole32.dll", PreserveSig=false)]
private static extern IStorage StgCreateDocfile(
    [MarshalAs(UnmanagedType.LPWStr)]string pwcsName, 
    [In] uint grfMode, [In] uint reserved);
private static void SaveGraphToFile(IGraphBuilder graph, string path)
{
    using(DisposalCleanup dc = new DisposalCleanup())
    {
        string streamName = "ActiveMovieGraph";
        IPersistStream ps = (IPersistStream)graph;
        IStorage graphStorage = StgCreateDocfile(path,
            (uint)(STGM_CREATE | STGM_TRANSACTED | STGM_READWRITE | STGM_SHARE_EXCLUSIVE), 0);
        dc.Add(graphStorage);
        UCOMIStream stream = graphStorage.CreateStream(
            streamName, (uint)(STGM_WRITE | STGM_CREATE | STGM_SHARE_EXCLUSIVE), 0, 0);
        dc.Add(stream);
        ps.Save(stream, true);
        graphStorage.Commit(0);
    }
}

However, the main purpose of GraphPublisher, and the reason it’s used in a using block, is to publish the live graph to GraphEdit. GraphEdit allows you to connect to a remote graph exposed from another process, as long as that graph has been published to the Running Object Table (ROT), a globally accessible look-up table that keeps track of running objects. Not only does GraphEdit allow you to view and examine a live filter graph in another process, it frequently allows you to control it, too.

The publication of the graph to the ROT is done using the following code:

private class RunningObjectTableCookie : IDisposable
{
    private int _value;
    private bool _valid;

    internal RunningObjectTableCookie(int value)
    {
        _value = value;
        _valid = true;
    }

    ~RunningObjectTableCookie() { Dispose(false); }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (_valid)
        {
            RemoveGraphFromRot(this);
            _valid = false;
            _value = -1;
        }
    }

    internal bool IsValid { get { return _valid; } set { _valid = value; } }

    private static RunningObjectTableCookie AddGraphToRot(IGraphBuilder graph)
    {
        if (graph == null) throw new ArgumentNullException("graph");
        UCOMIRunningObjectTable rot = null;
        UCOMIMoniker moniker = null;
        try
        {
            // Get the ROT
            rot = GetRunningObjectTable(0);

            // Create a moniker for the graph
            int pid;
            using(Process p = Process.GetCurrentProcess()) pid = p.Id;
            IntPtr unkPtr = Marshal.GetIUnknownForObject(graph);
            string item = string.Format("FilterGraph {0} pid {1}",
                ((int)unkPtr).ToString("x8"), pid.ToString("x8"));
            Marshal.Release(unkPtr);
            moniker = CreateItemMoniker("!", item);

            // Registers the graph in the running object table
            int cookieValue;
            rot.Register(ROTFLAGS_REGISTRATIONKEEPSALIVE, graph, moniker, out cookieValue);
            return new RunningObjectTableCookie(cookieValue);
    }
    finally
    {
        // Releases the COM objects
        if (moniker != null) while(Marshal.ReleaseComObject(moniker)>0);
        if (rot != null) while(Marshal.ReleaseComObject(rot)>0);
    }
}

private static void RemoveGraphFromRot(RunningObjectTableCookie cookie)
{
    if (!cookie.IsValid) throw new ArgumentException("cookie");
    UCOMIRunningObjectTable rot = null;
    try
    {
        // Get the running object table and revoke the cookie
        rot = GetRunningObjectTable(0);
        rot.Revoke(cookie.Value);
        cookie.IsValid = false;
    }
    finally
    {
        if (rot != null) while(Marshal.ReleaseComObject(rot)>0);
    }
}

private const int ROTFLAGS_REGISTRATIONKEEPSALIVE = 1;

[DllImport("ole32.dll", ExactSpelling=true, PreserveSig=false)]
private static extern UCOMIRunningObjectTable GetRunningObjectTable(int reserved);

[DllImport("ole32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, PreserveSig=false)]
private static extern UCOMIMoniker CreateItemMoniker([In] string lpszDelim, [In] string lpszItem);

In its constructor, GraphPublisher adds the graph to the ROT using AddGraphToRot, storing the resulting cookie. In its IDisposable.Dispose method, GraphPublisher removes the graph from the ROT by passing the stored cookie to RemoveGraphFromRot.

Unmanaged Resource Cleanup

It’s very important to release resources as soon as possible after you’ve finished using them. This is especially important when using DirectShow COM objects that work with large amounts of audio and video resources. Forcing the disposal of a COM object can be done using the Marshal.ReleaseComObject method, which decrements the reference count of the supplied runtime callable wrapper. When the reference count reaches zero, the runtime releases all of its references on the unmanaged COM object (for more information on Marshal.ReleaseComObject, see the MSDN documentation on the method at http://msdn.microsoft.com/library/en-us/cpref/html/frlrfsystemruntimeinteropservicesmarshalclassreleasecomobjecttopic.asp). Rather than having my code littered with try/finally blocks for each COM object in use, I created a helper class called DisposalCleanup that simplifies lifetime management of COM objects:

public class DisposalCleanup : IDisposable
{
    private ArrayList _toDispose = new ArrayList();
    public void Add(params object [] toDispose)
    {
        if (_toDispose == null) throw new ObjectDisposedException(GetType().Name);
        if (toDispose != null)
        {
            foreach(object obj in toDispose)
            {
                if (obj != null && (obj is IDisposable || obj.GetType().IsCOMObject || obj is IntPtr))
                {
                    _toDispose.Add(obj);
                }
            }
        }
    }

    void IDisposable.Dispose()
    {
        if (_toDispose != null)
        {
            foreach(object obj in _toDispose) EnsureCleanup(obj);
            _toDispose = null;
        }
    }

    private void EnsureCleanup(object toDispose)
    {
        if (toDispose is IDisposable)
        {
            ((IDisposable)toDispose).Dispose();
        }
        else if (toDispose is IntPtr) // IntPtrs must be interface ptrs
        {
            Marshal.Release((IntPtr)toDispose);
        }
        else if (toDispose.GetType().IsCOMObject)
        {
            while (Marshal.ReleaseComObject(toDispose) > 0);
        }
    }
}

The important method here is EnsureCleanup, which is called from the DisposalCleanup’s IDisposable.Dispose method. Called for each object that was added to DisposalCleanup using its Add method, EnsureCleanup calls Dispose on an IDisposable object, calls Marshal.ReleaseComObject on a COM object, and calls Marshal.Release on an interface pointer. With this, all my code has to do is surround a block of code that uses lots of COM objects with a using block that creates a new DisposalCleanup, add any COM objects or interfaces to the DisposalCleanup instances, and when the using block ends, the DisposalCleanup’s IDisposable.Dispose method will be called, releasing all of the used resources. My base Converter class implements this scheme and exposes the constructed DisposalCleanup through a protected DisposalCleanup property.

public object Convert()
{
    _cancellationPending = false;
    try
    {
        object result;
        using(_dc = new DisposalCleanup())
        {
             // Do the actual work
            result = DoWork();
        }
        OnConversionComplete(null, result);
        return result;
    }
    catch(DirectShowException exc)
    {
        OnConversionComplete(exc, null);
        throw;
    } 
    catch(Exception exc)
    {
        exc = new DirectShowException(exc);
        OnConversionComplete(exc, null);
        throw exc;
    }
    catch
    {
        OnConversionComplete(new DirectShowException(), null);
        throw;
    }
}

private DisposalCleanup _dc;
protected DisposalCleanup DisposalCleanup { get { return _dc; } }

The DoWork method is abstract, and in the case of the WmvConverter class, builds the filter graph and calls the RunGraph method. This way, a derived class can implement DoWork and simply add disposable objects to the base class’ DisposalCleanup; those resources will be disposed of automatically by the base class after the derived class’ work has been performed, even if it throws an exception.

Putting WmvConverter to use: WmvTranscoderPlugin

With the previously discussed code, you can obviously write a wealth of applications that process and convert DVR-MS files into WMV files. But the most common request I’ve seen for this functionality is as part of a Media Center-integrated solution. Multiple, very useful solutions have been created as a result, most notably dCut by Dan Giambalvo (available for download at http://www.inseattle.org/~dan/Dcut.htm) and DVR 2 WMV by Alex Seigler, José Peña, James Edelen, and Jeff Griffin (available for download at http://www.thegreenbutton.com/downloads.aspx). Both of these applications rely on the dvr2wmv DLL written by Alex Siegler (using techniques very similar to those shown in this article, though in unmanaged code). These apps make very valiant attempts to integrate into Media Center, and more specifically to mimic the look and feel of the Media Center shell, but unfortunately the current Media Center SDK only allows for so much. Luckily, there’s another relatively unexplored area of the SDK that makes it easy to integrate this functionality into the Media Center UI while still retaining all of the great chrome already written by the Media Center team: ListMaker add-ins.

A ListMaker add-in is a managed component provided by a 3rd-party that runs inside the Media Center process, using API elements exposed from the Microsoft.MediaCenter.dll assembly (you can find this DLL in the %windir%\ehome directory on a Media Center system). A ListMaker add-in’s life is a simple one: its purpose is to take a list of files provided to it by Media Center and do something with that list (what it does is up to the add-in). Media Center has built into it the UI to handle the list making and to handle the display of progress updates as reported by the add-in while it does its processing of the list. The cool thing is that Media Center doesn’t care what the add-in does with the list of media. As such, you can write an add-in that converts each of the user-selected DVR-MS files to WMV, writing them to a folder on a hard disk. More specifically, I have (Figure 9), and I’ll show you how here.

9. WmvDiscWriter_RecentPrograms

Figure 9. WMV Transcoder Add-in.

First and foremost, a ListMaker add-in must derive from System.MarshalByRefObject, as must all add-ins for Media Center (unfortunately, this is not currently mentioned in the SDK documentation, but it’s extremely important). Media Center loads all add-ins into a separate application domain, which means it uses the .NET Remoting infrastructure to access the add-in across application domain boundaries. This is the purpose of the MarshalByRefObject class, which enables access to objects across application domain boundaries, and thus must be the base class for an add-in. If you forget to derive from MarshalByRefObject, your add-in will not load or run correctly.

In addition to deriving from MarshalByRefObject, a ListMaker add-in also implements two main interfaces from the Microsoft.MediaCenter.dll assembly: Microsoft.MediaCenter.AddIn.IAddInModule and Microsoft.MediaCenter.AddIn.ListMaker.ListMaker:

public class WmvTranscoderPlugin : MarshalByRefObject, IAddInModule, ListMakerApp, IBrandInfo
{
    ...
}

IAddInModule is implemented by all Media Center add-ins and allows for initialization and disposal code to be run by implementing the IAddInModule.Initialize and IAddInModule.Uninitialize methods. In many scenarios, very little if anything has to be done in the initialization phase; for my add-in, I simply look in the registry to find user preferences for things like to which disk transcoded files should be written (the PreferredDrive value on the HKLU\Software\Toub\WmvTranscoderPlugin key in the registry) and which Windows Media profile should be used for transcoding to WMV (the ProfilePath value on the HKLU\Software\Toub\WmvTranscoderPlugin key in the registry). If no drive is specified (or if the specified drive is invalid), I default to the first valid drive returned from System.IO.Directory.GetLogicalDrives, where valid is defined to be any drive for which the Win32 GetDriveType function states that the drive is a fixed drive.

ListMakerApp is the main interface for the list making process and serves dual purposes: to allow the user to select the set of media files to be processed (Figure 10), and to start the add-ins processing, after which point it allows the Media Center UI to report progress (Figure 11).

10. WmvDiscWriter_SelectingShows

Figure 10. Selecting shows to transcode.

11. WmvDiscWriter_ProgressUpdates

Figure 11. Progress updates in the Media Center shell.

The members involved in the former aren’t extremely exciting, so I won’t spend much time covering them. Basically, Media Center calls into the add-in through this interface to get information about how many DVR-MS files have be selected, how many more can be added, and calls into it every time the user changes the list of items to be processed. The core of this is handled by three methods:

public void ItemAdded(ListMakerItem item)
{
    _itemsUsed++;
    _bytesUsed += item.ByteSize;
    _timeUsed += item.Duration;
}

public void ItemRemoved(ListMakerItem item)
{
    _itemsUsed--;
    _bytesUsed -= item.ByteSize;
    _timeUsed -= item.Duration;
}

public void RemoveAllItems()
{
    _itemsUsed = 0;
    _bytesUsed = 0;
    _timeUsed = TimeSpan.FromSeconds(0);
}

The captured information is then exposed through other properties and methods such as the following:

public TimeSpan TimeUsed { get { return _timeUsed; } }
public int ItemUsed { get { return _itemsUsed; } }
public long ByteUsed { get { return _bytesUsed; } }
public TimeSpan TimeCapacity { get { return TimeSpan.MaxValue; } }
public int ItemCapacity { get { return int.MaxValue; } }
public long ByteCapacity { get { return (long)GetFreeSpace(_selectedDrive); } }

The Used methods simply return the counts maintained by the methods mentioned above. The TimeCapacity and ItemCapacity properties both return their types’ respective MaxValue values, since computing the actual amount of time and the actual number of available items is much beyond the scope of this article. ByteCapacity uses my private GetFreeSpace method (which is, again, simply a p/invoke wrapper for the Win32 GetDiskFreeSpaceEx function) to return the amount of space available on disk; of course, this is also a fairly useless number in coordination with ByteUsed, as ByteUsed represents the size of DVR-MS files and ByteCapacity is used to determine whether there’s room on disk for these files, but the output files will be compressed WMV files. Regardless, it’s an implementation detail that you should feel free to change.

There are three more important but simple to implement properties that I’d like to point out:

public MediaType SupportedMediaTypes { get { return MediaType.RecordedTV; } }
public bool OrderIsImportant { get { return true; } }
public IBrandInfo BrandInfo { get { return this; } }

SupportedMediaTypes returns a flagged enumeration listing the types of media that are supported by this add-in: the possible types include pictures, videos, music, recorded television, etc., all of the types of media generally supported by Media Center. Since this add-in focuses specifically on converting DVR-MS files to WMV files, however, I’ve implemented it to only return MediaType.RecordedTV from SupportedMediaTypes.

OrderIsImportant is used by Media Center to determine whether it should allow the user to reorder the list of recorded shows to be processed. While the order isn’t actually important for this add-in (since it’s just writing the files to the hard disk), I did want to allow a user to schedule certain shows for conversion before others (Figure 12), so I return true rather than false from this property.

12. WmvDiscWriter_Reorder

Figure 12. Reordering selected shows.

The BrandInfo property allows for the author of an add-in to modify the UI displayed by Media Center to include product-specific information. The property returns an object that implements the IBrandInfo interface. For simplicity, I simply implement that interface on my add-in and return a reference to the add-in object itself:

public class WmvTranscoderPlugin : MarshalByRefObject, IAddInModule, ListMakerApp, IBrandInfo
{
    ...
    public IBrandInfo BrandInfo { get { return this; } }
    ...
    public string ViewListPageTitle { get { return "Files to transcode"; } }
    public string SaveListButtonTitle { get { return "Transcode"; } }
    public string PageTitle { get { return "Transcode to WMV"; } }
    public string CreatePageTitle { get { return "Specify target folder"; } }
    public string ViewListButtonTitle { get { return "View List"; } }
    public string ViewListIcon { get { return null; } }
    public string MainIcon { get { return null; } }
    public string StatusBarIcon { get { return null; } }
    ...
}

The eight properties on IBrandInfo are split into two categories: text strings that are rendered in the UI, and path strings that specify the location of graphics on disk. If a property returns null, the default value is used. So, since my graphic artist skills are a bit lacking at the moment, I’ve returned null from all of the icon properties. Where these properties come into play in the UI is shown in the following table:

Property Description
PageTitle The text in the upper-right corner while the add-in is in use.
CreatePageTitle The title text of the list creation page.
SaveListButtonTitle The text on the button that is used to start the processing operation once the list has been created.
ViewListButtonTitle The text on the button that is used to view the media items to be copied to be processed.
ViewListPageTitle The title text of the view-list page.
MainIcon The path to the file containing the icon to use as the main icon (watermark) on the list-making page.
StatusBarIcon The path to the file containing the icon that Media Center places in the lower-left corner of the creation pages.
ViewListIcon The path to the icon file that Media Center places at the top of the view-list page.

The most interesting methods on ListMakerApp are Launch and Cancel. Once the user has created the list of files to be processed and clicks the button to start the process, Media Center calls the Launch method supplying three arguments: the list of recorded shows the user selected, the progress update delegate that can be called to inform Media Center of a status update, and the completed delegate that should be called to inform Media Center that the process has completed (either successfully or due to an exceptional condition). The Launch method is meant to return immediately, doing the actual work on a background thread. The Cancel method is called when the user selects to cancel the process, and it is then up to the add-in to cease and desist its operation.

WmvTranscoderPlugin’s implementation follows this pattern, storing to member variables the arguments to Launch and then queuing to the ThreadPool the method, ConvertToWmv, that performs the actual conversion work:

public void Launch(ListMakerList lml, ProgressChangedEventHandler pce, CompletionEventHandler ce)
{
    _listMakerList = lml;
    _progressChangedHandler = pce;
    _completedHandler = ce;
    _cancellationPending = false;
    ThreadPool.QueueUserWorkItem(new WaitCallback(ConvertToWmv), null);
}

private void ConvertToWmv(object ignored)
{
    ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority;
    Thread.CurrentThread.Priority = ThreadPriority.Lowest;
    try
    {
        DirectoryInfo outDir = Directory.CreateDirectory(_selectedDrive + ":\\" + _listMakerList.ListTitle);
        _currentConvertingIndex = 0;
        foreach(ListMakerItem item in _listMakerList.Items)
        { 
            if (_cancellationPending) break;
            string dvrMsName = item.Filename;
            string wmvName = outDir.FullName + "\\" + item.Name + ".wmv";


            _currentConverter = new WmvConverter( dvrMsName, wmvName, _profilePath);
            _priorCompletedPercentage = _currentConvertingIndex / (float)_listMakerList.Count;
            _currentConverter.PollFrequency = 2000;
            _currentConverter.ProgressChanged += new ProgressChangedEventHandler(ReportChange);
            _currentConverter.Convert();
            _currentConverter = null;
            _currentConvertingIndex++;
        }
        _completedHandler(this, new CompletionEventArgs());
    }
    catch(Exception exc)
    {
         _completedHandler(this, new CompletionEventArgs(exc));
    }
    finally
    { 
        Thread.CurrentThread.Priority = oldThreadPriority;
    }
}

ConvertToWmv creates a directory on the selected drive, using the name of the target folder as specified by the user (see Figure 13). The method then iterates over all of the ListMakerItem objects in the supplied ListMakerList, getting the path to the DVR-MS file and using the WmvConverter I built earlier to convert each DVR-MS file to a WMV file in the target directory. The Converter’s ProgressChanged event is wired to a private method in the add-in, ReportChange, that in turn calls to Media Center’s progress update delegate. Additionally, the current converter is stored to a member variable so that the Cancel method can be used to halt its progress.

13. WmvDiscWriter_TargetFolder

Figure 13. Specifying a target folder.

The Cancel method is also very straightforward. It sets a member variable to alert the ConvertToWmv method running on another thread that the user has requested cancellation. However, as you can see in the ConvertToWmv method, this won’t be checked until the method is about to start converting the next DVR-MS file, so the Cancel method also uses the WmvConverter object stored in a member variable to cancel the currently executing conversion using the Converter’s CancelAsync method. As we saw before, this will cause the Converter.RunGraph method to halt as soon as it returns from the WaitForCompletion method.

public void Cancel()
{
    // Cancel any pending conversions
    _cancellationPending = true;

    // Cancel the current conversion
    WmvConverter converter = _currentConverter; 
    if (converter != null) converter.CancelAsync();
}

I’ve included in the download for this article a fully working implementation of this add-in, including an installer. The installer installs both WmvTranscoderPlugin’s assembly and WmvConverter’s assembly into the Global Assembly Cache (GAC) and then uses the RegisterMceApp.exe tool to inform Media Center of this add-in. The registration application relies on an XML configuration file, like the one shown here:

<application title="WMV Transcoder" id="{50d449ee-c06d-43e3-a94a-48b8eed72968}">
    <entrypoint id="{a60de2e7-cade-48e3-8eb1-6f9ca898408a}"
        addin="Toub.MediaCenter.Tools.WmvTranscoderPlugin,
                    Toub.MediaCenter.Tools.WmvTranscoderPlugin,
                    Version=1.0.0.0, PublicKeyToken=6e541e2c6f2c93d2, Culture=neutral"
        title="WMV Transcoder"
        description="Transcodes recorded shows to WMV"
        imageURL=".\WmvTranscoderPlugin.png">
    <category category="ListMaker\ListMakerApp"/>
    </entrypoint>
</application>

You should be able to run the installer and start converting from DVR-MS to WMV right away, directly from a very snazzy UI that neither of us had to write (thanks, Media Center team!)

14. WmvDiscWriter_Completed

Figure 14. Successful transcoding.

Accessing DVR-MS Metadata

The DVR-MS file format contains audio, video, and closed captioning data, but it also contains metadata describing the file and its contents. This is where information such as a television show’s title, description, cast, and original air date are stored once the show has been recorded. What’s cool is that this data is easily accessible to your own applications through the IStreamBufferRecordingAttribute interface, which is implemented by the DirectShow StreamBufferRecordingAttribute object. This object can be created using its CLSID as I’ve done with other DirectShow objects in this article.

To use the IStreamBufferRecordingAttribute, I first have to provide a managed interface for it (you’ll find this code nested in the DvrmsMetadataEditor class in the code download for this article):

[ComImport]
[Guid("16CA4E03-FE69-4705-BD41-5B7DFC0C95F3")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IStreamBufferRecordingAttribute
{
    void SetAttribute(
        [In] uint ulReserved,
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,
        [In] MetadataItemType StreamBufferAttributeType,
        [In, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,
        [In] ushort cbAttributeLength);

    ushort GetAttributeCount([In] uint ulReserved);

    void GetAttributeByName( 
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszAttributeName,
        [In] ref uint pulReserved,
        [Out] out MetadataItemType pStreamBufferAttributeType,
        [Out, MarshalAs(UnmanagedType.LPArray)] byte[] pbAttribute,
        [In, Out] ref ushort pcbLength);

    void GetAttributeByIndex (
        [In] ushort wIndex,
        [In, Out] ref uint pulReserved,
        [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszAttributeName,
        [In, Out] ref ushort pcchNameLength,
        [Out] out MetadataItemType pStreamBufferAttributeType,
        [Out, MarshalAs(UnmanagedType.LPArray)] byte [] pbAttribute,
        [In, Out] ref ushort pcbLength); 

    IntPtr EnumAttributes();
}

To access the metadata for a DVR-MS file, I construct a StreamBufferRecordingAttribute object and obtain its IFileSourceFilter interface (you saw the counterpart IFileSinkFilter interface earlier in this article; they’re almost exactly the same). The IFileSourceFilter’s Load method is used to open the DVR-MS file in whose metadata I’m interested, at which point its IStreamBufferRecordingAttribute interface can be obtained and used to retrieve and edit the metadata:

public class DvrmsMetadataEditor : MetadataEditor
{
    IStreamBufferRecordingAttribute _editor;

    public DvrmsMetadataEditor(string filepath)
    {
        IFileSourceFilter sourceFilter = (IFileSourceFilter)
            ClassId.CoCreateInstance(ClassId.RecordingAttributes);
        sourceFilter.Load(filepath, null);
        _editor = (IStreamBufferRecordingAttribute)sourceFilter;
    }
    ...
}

Read access to the metadata is provided through the DvrmsMetadataEditor.GetAttributes method, which provides a thin abstraction over IStreamBufferRecordingAttribute’s GetAttributeCount and GetAttributeByIndex methods.

public override System.Collections.IDictionary GetAttributes()
{
    if (_editor == null) throw new ObjectDisposedException(GetType().Name);
    Hashtable propsRetrieved = new Hashtable();
    ushort attributeCount = _editor.GetAttributeCount(0);
    for(ushort i = 0; i < attributeCount; i++)
    {
        MetadataItemType attributeType;
        StringBuilder attributeName = null;
        byte[] attributeValue = null;
        ushort attributeNameLength = 0;
        ushort attributeValueLength = 0;
        uint reserved = 0;

        _editor.GetAttributeByIndex(i, ref reserved, attributeName,
            ref attributeNameLength, out attributeType, attributeValue, ref attributeValueLength);
        attributeName = new StringBuilder(attributeNameLength);
        attributeValue = new byte[attributeValueLength];
        _editor.GetAttributeByIndex(i, ref reserved, attributeName, ref attributeNameLength, 
            out attributeType, attributeValue, ref attributeValueLength);
        if (attributeName != null && attributeName.Length > 0)
        {
            object val = ParseAttributeValue(attributeType, attributeValue);
            string key = attributeName.ToString().TrimEnd('\0');
            propsRetrieved[key] = new MetadataItem(key, val, attributeType);
        }
    }
    return propsRetrieved;
}

First, the GetAttributeCount method is used to find out how many metadata items there are to be retrieved. Then, for each attribute, the length of the name of the attribute and the length (in bytes) of the value are retrieved using the GetAttributeByIndex method (by specifying a null value for both the name and value parameters). With the lengths in hand, I can create buffers appropriately large in size to store the data, and I can call GetAttributeByIndex again to retrieve the actual name and byte array value for the attribute. If it is successfully retrieved, the byte array storing the value is then parsed into the appropriate managed object, based on the type of the attribute. My ParseAttributeValue method returns a GUID, unsigned integer, unsigned long, unsigned short, string, Boolean, or the original array if the value is simply a binary blob, common for most complex metadata attributes. The name of the attribute along with its type and value are then used to construct a new MetadataItem instance, which is added to a Hashtable of all the attributes for the file. When all of the attributes have been retrieved, this collection is returned to the user.

The SetAttributes method works in the reverse fashion. It is supplied with a collection of MetadataItem objects, each of which is formatted into the appropriate byte array base on its type, which is then used in conjunction with the SetAttribute method to set the metadata attribute on the file:

public override void SetAttributes(IDictionary propsToSet)
{
    if (_editor == null) throw new ObjectDisposedException(GetType().Name);
    if (propsToSet == null) throw new ArgumentNullException("propsToSet");

    byte [] attributeValueBytes; 
    foreach(DictionaryEntry entry in propsToSet)
    {
        MetadataItem item = (MetadataItem)entry.Value;
        if (TranslateAttributeToByteArray(item, out attributeValueBytes))
        {
            try
            {
                _editor.SetAttribute(0, item.Name, item.Type, attributeValueBytes,
                    (ushort)attributeValueBytes.Length);
            }
            catch(ArgumentException){}
            catch(COMException){}
        }
    }
}

MetadataItem is a simple wrapper around a name of an attribute, the value of the attribute, and the type of the attribute. MetadataItemType is an enumeration of valid types (GUID, string, unsigned integer, etc.).

You might notice that the DvrmsMetadataEditor class derives from a base MetadataEditor class. I’ve done this so as to provide another class, AsfMetadataEditor, which also derives from MetadataEditor. AsfMetadataEditor is based on sample code included in the Windows Media Format SDK (available for download at http://download.microsoft.com/download/9/F/D/9FDFB288-B4BF-45FA-959C-1CC6D909AA92/WMFormat95SDK.exe). It uses the Windows Media IWMMetadataEditor and IWMHeaderInfo3 interfaces to obtain metadata information about WMA and WMV files, both of which are based on the ASF file format. You may discover that you can currently use these Windows Media Format SDK interfaces to work with DVR-MS files in addition to with WMA and WMV files, however in the future that may not be the case, and Microsoft strongly encourages the use of the IStreamBufferRecordingAttribute interface for this purpose. The relevant portions of the IWMHeaderInfo3 interface are almost identical to the IStreamBufferRecordingAttribute interface, and thus the AsfMetadataEditor and DvrmsMetadataEditor classes are also strikingly similar.

With these classes in place, it becomes trivial to copy the metadata from one media file to another, such as from a DVR-MS file to a transcoded WMV file, allowing you to preserve the fidelity of the metadata associated with a transcoded TV recording:

using(MetadataEditor sourceEditor = new DvrmsMetadataEditor(srcPath))
{
    using(MetadataEditor destEditor = new AsfMetadataEditor(dstPath))
    {
        destEditor.SetAttributes(sourceEditor.GetAttributes());
    }
}

In fact, for the very purpose of copying metadata from one media file to another, I’ve created a static MigrateMetdata method on the MetadataEditor class, which not only migrates the metadata as shown above, but also augments it such that more applicable information is shown when a DVR-MS file is viewed in Media Player and when a WMV file is played in Media Center.

Editing DVR-MS Files

Second to converting to WMV, editing and splicing DVR-MS files is probably the second most requested feature I’ve see in newsgroups around the web. What many people don’t realize is that splicing functionality is provided out of the box through the DirectShow RecComp object and its IStreamBufferRecComp interface. The IStreamBufferRecComp interface is used to create new recordings from pieces of existing recordings, concatenating segments together from one or more DVR-MS files.

The IStreamBufferRecComp interface is very straightforward, and a C# import of it is shown here:

[ComImport]
[Guid("9E259A9B-8815-42ae-B09F-221970B154FD")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStreamBufferRecComp
{
    void Initialize(
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetFilename,
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecProfileRef);

    void Append(
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording);

    void AppendEx(
        [In, MarshalAs(UnmanagedType.LPWStr)] string pszSBRecording,
        [In] ulong rtStart, [In] ulong rtStop);

    uint GetCurrentLength();

    void Close();

    void Cancel();
}

To splice DVR-MS files, first create an instance of the RecComp object. This can be done using the ClassId.CoCreateInstance method demonstrated earlier in this article, with code like:

IStreamBufferRecComp recCom =
    (IStreamBufferRecComp)ClassId.CoCreateInstance(ClassId.RecComp);

and with ClassId.RecComp defined as

public static readonly Guid RecComp =
    new Guid("D682C4BA-A90A-42FE-B9E1-03109849C423");

With an IStreamBufferRecComp in hand, its Initialize method can then be used to specify the output file name for the new recording. Additionally, the second parameter to Initialize should be the file path to one of the input DVR-MS files that will be spliced. IStreamBufferRecComp supports the concatenation of segments from one or more files, but all of those files must have been recorded using the same profile, which means they must have been recorded using the same configuration and settings in Media Center. RecComp needs to know what profile to use for the output file, and thus you must specify one of the input files as the second parameter so that it can examine its profile information and use that as a basis for the output file.

Once the IStreamBufferRecComp has been initialized, you can start building up the new file. Call the Append method, specifying the full path to an input DVR-MS file, and that entire file will be appending to the output file. The AppendEx method allows you to specify additional starting and stopping times such that only a portion of the input file will be used and appended to the output file. These times in the unmanaged interface are defined as REFERENCE_TIME, a 64-bit long value that represents the number of 100 nanosecond units, so in managed code you can use a function like the following to convert from seconds to the REFERENCE_TIME value passed to AppendEx:

internal static ulong SecondsToHundredNanoseconds(double seconds)
{
    return (ulong)(seconds * 10000000);
}

When you’re done appending to the output file, the Close method closes the output file. While you’re concatenating to the file, you can use the GetCurrentLength method from a separate thread to find out the current length of the output file. You can then use this information, along with your knowledge of the lengths of the input files/segments, to compute what percentage of the splicing has been completed. Note that this process is extremely fast, as no encoding or decoding is necessary in order to append segments from one DVR-MS file to another.

As a demonstration of this interface, I built the DVR-MS Editor application, shown in Figure 15, and available as part of the code download associated with this article.

15. DvrMsEditor

Figure 15. DVR-MS Editor.

The application is actually very simple and was implemented in little more than an hour. The Windows Media Player ActiveX control is used to show input video files. To load a video file, the AxWindowsMediaPlayer.URL property is set to the path to the DVR-MS file, causing Media Player to load the video (and to start playing it if the AxWindowsMediaPlayer.settings.autoStart property is true).

Once the video is loaded, a user can control it using the Media Player toolbar, which allows the user complete control over the playing and seeking of the video. When it’s at a location at which the user would like to start or stop a segment, the AxWindowsMediaPlayer.Ctlcontrols.currentPosition property is queried. Those times can then be used with the IStreamBufferRecComp interface just described to create the output file.

Additionally, Media Player provides fine-grained programmatic control over the current position of the video. You can advance the video frame by frame using code such as the following:

((WMPLib.IWMPControls2)player.Ctlcontrols).step(1);

Alternatively, you can jump to a particular location in the video by setting the AxWindowsMediaPlayer.Ctlcontrols.currentPosition that was discussed a moment ago.

The DVR-MS Editor application also takes advantage of some of the other techniques described previously in the article, such as copying metadata from the source video files to the output video file.

Conclusion

Pretty amazing stuff, right? The DirectShow and Windows XP Media Center Edition teams have provided developers with a vast array of tools for working with DVR-MS files, both in unmanaged and managed code. These tools make it possible to create new applications that allow for really powerful functionality most people don’t realize is available to them. The topics discussed in this article represent only a portion of the types of things you can do with DVR-MS files, and an even smaller number of solutions one can write that makes use of these libraries and tools. I’d love to hear about solutions you develop using this functionality.

Now, go watch some TV.

Related Books

· Programming Microsoft® DirectShow® for Digital Video and Television (MSPress, 2003)
· Fundamentals of Audio and Video Programming for Games (MSPress, 2003)

Acknowledgements

My sincere thanks to Matthijs Gates, Aaron DeYonker, Ryan D'Aurelio, Ethan Zoller, Eric Gunnerson, Charlie Owen, and Alex Seigler for their subject matter expertise, to ABC for permitting me the use of examples and screenshots from their television programming, and to my good friends John Keefe and Eden Riegel for letting me use their likeness in this article.