Stephen Toub
Microsoft Corporation
March 2005
Applies to:
Microsoft Windows XP Media
Center Edition 2005
Microsoft
DirectShow
DirectX 9.0 SDK
Summary: Stephen Toub discusses the DVR-MS file
format generated by Windows XP Media Center 2005, provides an
introduction to DirectShow, and shows how the latter can be
used to work with the former. (47 printed pages)
Download
the DVR-MS Sample Code.msi.
Contents
Playing
DVR-MS Files
Introduction
to DirectShow and GraphEdit
DirectShow
Interfaces
Transcoding
to WMV
Debugging
Filter Graphs
Unmanaged
Resource Cleanup
Putting
WmvConverter to Use: WmvTranscoderPlugin
Accessing
DVR-MS Metadata
Editing
DVR-MS Files
Conclusion
Related
Books
Acknowledgements
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 from managed code. 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 and
that you're using NTSC, non-HD content (while most of the
concepts discussed apply to PAL and HD, the sample code may
not work correctly with those formats). 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. Finally, the code samples and
applications associated with this article were compiled
targeting the .NET Framework 1.1. However, Windows XP Media
Center 2005 does not come with the .NET Framework 1.1
installed by default, but rather 1.0. Thus, in order to use
these samples on your Media Center, you must either install
the .NET Framework 1.1 (available from Windows Update) or
recompile the samples for the .NET Framework
1.0.
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.
.gif)
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 that 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 shut down,
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 that
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.
.gif)
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();
}
.gif)
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:
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 download. (For material
covered later in this article, you'll also need the February
2005 Extras download.) 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.
.gif)
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);
}
.gif)
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 that 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.
.gif)
Figure 6. GraphEdit ready to play a DVR-MS
file (click to enlarge)
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.
.gif)
Figure 7. Incorporating closed captions
into video display (click to enlarge)
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#, 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 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 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:
- Open GraphEdit.
- 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.
- 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.
.gif)
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 is
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 that 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, 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.) 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
third-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.
.gif)
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 HKLM\Software\Toub\WmvTranscoderPlugin key in
the registry) and which Windows Media profile should be used
for transcoding to WMV (the ProfilePath
value on the HKLM\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-in's processing, after which point it allows
the Media Center UI to report progress (Figure 11).
.gif)
Figure 10. Selecting shows to transcode
.gif)
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.
.gif)
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.
.gif)
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!)
.gif)
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);
[return: MarshalAs(UnmanagedType.Interface)]
object 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 (download
the SDK here). 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 made it available
as part of the code download associated with this article.
.gif)
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 (Microsoft Press, 2003)
- Fundamentals of Audio and Video Programming for Games
(Microsoft Press, 2003)
Acknowledgements
My sincere thanks to Matthijs Gates, Aaron DeYonker, Ryan
D'Aurelio, Ethan Zoller, Eric Gunnerson, 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.
About the author
Stephen Toub is the Technical Editor for MSDN
Magazine, for which he also writes the .NET Matters
column.