Introduction
If you've seen my previous article, How to Create Optical File Images using IMAPIv2.0, you might expect me to do something with that file image, like... burn it. It is the same Image Mastering API v2.0 I'm using to achieve this goal, so you might want to install it before running this application. There might be other burning engines available, but I've chosen to use this API because it is likely to have long term support from Microsoft. Using COM Interop to wrap the native components has some quirks I've tried to fix, but it is not very difficult, thanks to the samples from Microsoft. If you were interested only in burning folders and files on to media and skip the file image creation, you will be better served by Eric Haddan's great article. I recommend you check the articles mentioned above should you need a better understanding of the inner workings or COM Interop issues. I've made only minor changes to the code from my previous article, and I also got some cues from the sources I have mentioned below.
Background
If you are wondering why you would need to burn an ISO image when you could cold burn the files directly, then you should be aware that you cannot easily create every kind of format using IMAPI 2.0. However, there is no problem in burning a file stream if the recorder supports the media type. In other words, if you have an ISO file containing a DVD movie, you will be able to burn it, but you can't create a proper DVD disk even if you copy all the files from the media. While you might be able to play the latter on your computer and a few new players, most DVD players won't play it, because they expect a customized UDF format. This is the case when burning an existing ISO file image created with other tools overcomes this issue.
Burning a File Optical Image
Burning an optical image boils down to writing a stream on your media. The code for writing and formatting is similar to the code in Eric's article, with a few changes. Getting the stream interface is done using the ImageRepository.LoadCOMStream
that was described in my previous article. I've packaged most of the burning capability in a helper class called BurnHelper
, and below is its WriteStream
method:
public int WriteStream(System.Runtime.InteropServices.ComTypes.IStream stream,
string initburner, string clientName,
bool forceMediaToBeClosed, int speed, bool eject)
{
MsftDiscRecorder2 discRecorder = null;
MsftDiscFormat2Data discFormat = null;
int result = -1;
try
{
discRecorder = new MsftDiscRecorder2();
discRecorder.InitializeDiscRecorder(initburner);
discFormat = new MsftDiscFormat2Data();
discFormat.Recorder = discRecorder;
discRecorder.AcquireExclusiveAccess(true, clientName);
discFormat.ClientName = clientName;
discFormat.ForceMediaToBeClosed = forceMediaToBeClosed;
discFormat.Update += DiscFormatData_Update;
discFormat.SetWriteSpeed(speed, true);
discFormat.Write(stream);
if (_backgroundWorker.CancellationPending)
{
return 1;
}
if (eject)
{
IMAPI_FORMAT2_DATA_MEDIA_STATE state =
IMAPI_FORMAT2_DATA_MEDIA_STATE.IMAPI_FORMAT2_DATA_MEDIA_STATE_UNKNOWN;
while (state == IMAPI_FORMAT2_DATA_MEDIA_STATE.
IMAPI_FORMAT2_DATA_MEDIA_STATE_UNKNOWN &&
!_backgroundWorker.CancellationPending)
{
try
{
state = discFormat.CurrentMediaStatus;
}
catch (Exception)
{
state = IMAPI_FORMAT2_DATA_MEDIA_STATE.
IMAPI_FORMAT2_DATA_MEDIA_STATE_UNKNOWN;
System.Threading.Thread.Sleep(3000);
}
}
if (!_backgroundWorker.CancellationPending)
discRecorder.EjectMedia();
}
result = 0;
}
finally
{
if (_backgroundWorker.CancellationPending)
result = 1;
if (discFormat != null)
{
discFormat.Update -= DiscFormatData_Update;
Marshal.FinalReleaseComObject(discFormat);
}
if (discRecorder != null)
{
discRecorder.ReleaseExclusiveAccess();
Marshal.FinalReleaseComObject(discRecorder);
}
}
return result;
}
If you see my code comments relative to Write Speed, you can notice that there might be speeds that are not shown as descriptors by IMAPI. You might be able to discover them by using other numbers than the ones provided by the descriptors that will show up in the progress update UI. I also had to delay the media being ejected until everything was flushed on the media, just to avoid a crash.
Detecting Media Changes
Synchronizing the media with the UI should not be taken for granted. Some systems might have the media notification turned off by Active Directory Policy or registry settings. I've originally tried using WMI to detect these events, but I did not get any notification when inserting blank media. Hence, I ended up using the old fashioned WM_DEVICECHANGE
Windows message to get the media changes. There is quite a bit of code to achieve this in the DeviceMonitor
class, which was based on this article as the starting point. Below is the most important method of this class:
protected override void WndProc(ref Message m)
{
const int WM_DEVICECHANGE = 0x0219;
switch (m.Msg)
{
case WM_DEVICECHANGE:
{
if (m.LParam == IntPtr.Zero)
break;
DEV_BROADCAST_VOLUME vol = (DEV_BROADCAST_VOLUME)
Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_VOLUME));
if ((vol.dbcv_devicetype == _dt) && (vol.dbcv_flags == _vf))
PostMessage(new HandleRef(this, Handle),
POST_MSG_DEVICECHANGE, m.WParam,
(IntPtr)vol.dbcv_unitmask);
}
break;
default:
if (POST_MSG_DEVICECHANGE == m.Msg)
{
DeviceEventBroadcast ev = (DeviceEventBroadcast)m.WParam.ToInt32();
List<char> drives = DriveNames((uint)m.LParam);
List<char>notifyDrives = new List<char>(1);
_drives.ForEach(delegate(char drv)
{
if (drives.Contains(drv)) notifyDrives.Add(drv);
});
if (DeviceAction != null && notifyDrives.Count > 0)
DeviceAction(notifyDrives, ev);
}
break;
}
base.WndProc(ref m);
}
If you are wondering why I needed to repost the original WM_DEVICECHANGE
to POST_MSG_DEVICECHANGE
, it is because WM_DEVICECHANGE
occurs before IMAPI becomes aware of the media change, and would crash otherwise.
Other Features
Unlike the previous version, I can't use the only UI thread by pumping messages with Application.DoEvents
because the IMAPI events are too scarce for a smooth user experience, so I had to use the BackgroundWorker
for burning. You have the option to format RW media, and sometimes even to force formatting unrecognizable media, a simple feature I've been unable to find in other burning programs. You'll also find many new features and enhancements when compared to my previous article. I have to mention that rebooting, sleeping, and the like are based on the mentalis code, and the making of the media bootable by adding a file has a bug I could not fix yet. If you want to take a stab at it, the code is in the ImageRepositoty.CreateBootDisc
method, so feel free to tinker with it. I tried both IMAPI2FS.FsiStream
and COMTypes.IStream
as arguments, but to no avail. If you find a way around, please let me know.
Adding a boot image option is done by checking the checkbox on the ‘Select Files’ tab of the application, as shown in the image below:
How to Use It
I did not test it extensively because of my limited environment, so what worked on my system might not work on yours. Creating an optical file image is the same as in my previous article sans the optional bootable option. Burning a file image from the ‘Build Image' tab should be quite easy, you have to select the image and burner, optionally the speed, eject, and close the media, and hit the 'Burn Image' button. You should be able to cancel the operation if you wish, but you might damage the media if it is not RW.
Reusing the Code
The solution has two projects, to separate the UI from the classes that actually do the image creation and burning. You can easily reuse this functionality if you add a reference to the ISOImage.dll or to the OpticalImage
project that comes in two flavours for Visual Studio 2005 and 2008. You must install the Image Mastering API v2.0 if you are not running on Vista, to use it.
History
- 16th May, 2008: Initial version
- 29th May, 2008: Version 2.0.0.1 - Fixed some UI and
VolumeName
bugs