It's been a long time since I've bored anyone with some programing, but given the dearth of information on use the Stream Buffer Engine (SBE) to create DVR-MS files (especially using managed code) I figured it was time to share; maybe save someone else the pain of figuring this out.
The code for this application can be downloaded here. Also this application uses the awesome DirectShow.Net library; so any derivative works must comply with it's licensing (LGPL).
FixDVRMSDuration is a very simple application that takes one DVR-MS file and uses DirectShow to stream the contents directly into another file. It was created to address issues with some files that had incorrect duration values. Unfortunetly the published interface for writing meta data (IStreamBufferRecordingAttribute) doesn't support writing the Duration value so this was the easiest way to address it.
The basic outline of the application looks like this:
- create a graph
- add the sbe sink filter
- add the source file to the graph (in this case a dvr-ms file)
- enumerate output pins on the source filter to render them (it's important to connect the pins before configuring the sink)
- configure the sbe sink
- run the graph
The only tricky bits are related to configuring the sbe sink filter so I'm going to skip the rest of the DirectShow stuff.
First step is to create an instance of the StreamBufferConfig object. We need to use two interfaces that the object implements IStreamBufferConfigure3 and IStreamBufferInitialize.
IStreamBufferConfigure3 config = (IStreamBufferConfigure3)new StreamBufferConfig();
IStreamBufferInitialize init = config as IStreamBufferInitialize;
When the SBE creates a file, it stores the configuration data in a registry location, so we need to create a new key. Creating it in HKCU ensures that anyone can use the program, and using a GUID makes sure there's no naming collision. Unfortunately the initialization interface requires a pointer to the key, so it's not possible (without using reflection) to get that from the managed RegistryKey class, so we can use the managed class to create the key and pinvoke RegOpenKeyEx to get the pointer.
string keyName = string.Format(@"Software\SBERender\{0}", Guid.NewGuid().ToString());
RegistryKey rk = Registry.CurrentUser.CreateSubKey(keyName, RegistryKeyPermissionCheck.ReadWriteSubTree);
rk.Close();
int result = RegOpenKeyEx(_registryHive, keyName, 0, STANDARD_RIGHTS_READ | KEY_QUERY_VALUE | STANDARD_RIGHTS_WRITE, out registryKey);
hr = init.SetHKEY(registryKey);
Once we set the registry location, we can tell the SBE where to write the backing files. The SBE was designed to create different types of recordings; when creating a temporary file (Live TV) the content is written to a file with the sbe extension located in this directory, when a permanent file is created (Recorded TV) the SBE still creates a sbe file in the this directory but the content is actually written to a dvr-ms file which can be located in a different directory.
hr = config.SetDirectory(Path.GetDirectoryName(target));
Honestly, I'm not sure why this next line is required. But without it bad things happen. Found out why this was necesary (in the documentation for IStreamBufferSink::LockProfile); "Windows Vista or later: This method requires administrator privileges, unless you first call IStreamBufferConfigure3::SetNamespace with the value NULL." Working with the SBE isn't hard, but the documentation is very opaque so figuring out the sequence and necessity of the calls was a PITA.
hr = config.SetNamespace(null);
Now that we've created the configuration, it's time to let the sink filter know about it. Retrieving the IStreamBufferInitialize interface from the sink and passing the pointer to our registry key does exactly that.
IStreamBufferInitialize init2 = sink as IStreamBufferInitialize;
hr = init2.SetHKEY(registryKey);
We get a glimpse into the DVR-MS's ASF heritage when we lock the profile to create the backing file.
hr = sink.LockProfile(Path.Combine(Path.GetDirectoryName(target),string.Format("{0}.sbe",Path.GetFileNameWithoutExtension(target))));
Recording is controlled through the IStreamBufferRecordControl interface which we get by calling CreateRecorder on the sink filter. In this case we want a permanent (dvr-ms) file so we tell the recorder to create a Content recording.
object objRecorder;
hr = sink.CreateRecorder(target, RecordingType.Content, out objRecorder);
IStreamBufferRecordControl rec = objRecorder as IStreamBufferRecordControl;
If we've setup the graph correctly it's time to start the recording. Pass in 0 to start immediately.
long start = 0;
hr = rec.Start(ref start);
Because of our application of the recording functionality, this is where we run the graph. Before stopping the graph it's necessary to stop the recording, pass in 0 to stop immediately. If the graph is stopped before the recording is, an error is generated.
hr = rec.Stop(0);
While this application is very simple, it does open the door to more interesting applications that generate dvr-ms files for existing content (DVD to DVR-MS for e.g.).