Fun with AudioVideoPlayback

I'm sure I'm not alone in that I keep a laundry list of personal coding projects I want to work on when I get free time.  Most of them are fairly short and only take a few hours to complete.  We finished work on the June issue of MSDN Magazine yesterday, leaving me with enough time to check off one of the items on my list.

The project involved Managed DirectX as I needed to be able to play a video file on a Form.  Doing this is incredibly straightfoward:

    Video v = Video.FromFile(filepath);
v.Owner = parentControl;
v.Play();

Couldn't be simpler.  Next, I wanted to be able to control the volume for the video, a task that's also very straightforward:

    Video v = Video.FromFile(filepath);
v.Owner = parentControl;
v.Play();
v.Audio.Volume = -10000; // silence

Piece of cake.  Later on, I stop the video:

    v.Stop();

and to remove the frozen image from the parent control on which the video was playing, I reassign the Video's owner to be a hidden picture box (thanks to Christopher for the idea):

    PictureBox hiddenBox = new PictureBox();
hiddenBox.Visible = false;
...
v.Owner = hiddenBox;

Again, too easy.  But here's where I hit a snag.  Microsoft.DirectX.AudioVideoPlayback.Video implements IDisposable which lets you immediately free the unmanaged resources held by the Video object.  I figured I could simply do:

    v.Dispose();

and be done with it.  (insert best buzzer noise here).   Unfortunately, even with this in place, the resources were not being free'd.  I looked at the disassembly for Video and noticed that while Microsoft.DirectX.AudioVideoPlayback.Audio (an instance of which is returned from Video.Audio) implements IDisposable, the Video.Dispose method was not calling Dispose on the instance of Audio that was returned to me in my previous usage of v.Audio.Volume.  It makes sense that both Audio and Video would hold onto unmanaged resources related to the video (in my case, an instance of my MPEG2 decoder was sticking around as was a huge amount of allocated memory), so I modified my code as follows:

    v.Audio.Dispose();
v.Dispose();

Didn't help.  Back to ildasm to view Video.get_Audio() (the get accessor for the Video object's Audio property):

    .method public specialname instance class Microsoft.DirectX.AudioVideoPlayback.Audio
get_Audio() cil managed
{
// Code size 16 (0x10)
.maxstack 4
.locals (class Microsoft.DirectX.AudioVideoPlayback.Audio V_0)
.try
{
IL_0000: ldarg.0
IL_0001: newobj
          instance void Microsoft.DirectX.AudioVideoPlayback.Audio::.ctor(
          class Microsoft.DirectX.AudioVideoPlayback.Video)
IL_0006: stloc.0
IL_0007: leave.s IL_000e
} // end .try
catch [mscorlib]System.Exception
{
IL_0009: pop
IL_000a: ldnull
IL_000b: stloc.0
IL_000c: leave.s IL_000e
} // end handler
IL_000e: ldloc.0
IL_000f: ret
} // end of method Video::get_Audio

This decompiles to C# something like the following (although I believe the original code was written in MC++):

    public Audio Audio
{
get
{
Audio tempAudio;
try
{
tempAudio = new Audio(this);
}
catch(Exception)
{
tempAudio = null;
}
return tempAudio;
}
}

I've highlighted in red the important lines.  No wonder calling v.Audio.Dispose() wasn't helping... in fact, it could very well have been making things worse.  Every call to v.Audio returns a new instance of Audio, so I was disposing a brand new object, not the one I originally set the volume on.  Ugh.  I had been expecting something more along the lines of:

    private Audio _audio;
...
public Audio Audio
{
get
{
if (_audio == null) _audio = new Audio(this);
return _audio;
}
}

where the Audio property would always return the same instance of Audio (assuming the underlying video hadn't changed) instead of creating a new one each time.  The fix? I simply changed my code to not only store a reference to the Video object v, but also to the initial result of v.Audio.  I then only use v.Audio when changing the volume in the future (which I do a few times).  When it's time to clean things up, instead of:

    v.Audio.Dispose();
v.Dispose();

I simply do:

    Audio a = v.Audio;
... do whatever I need to do with a instead of v.Audio...
a.Dispose();
v.Dispose();

And with that, the memory usage of my application returned to normal and the unnecessary instances of my MPEG2 decoder were released and went away.