
Introduction
Windows introduced the new IMAPIv2.0 with the release of the Vista Operating System which was a big improvement over the original IMAPI. The original IMAPI is great for CDROMs, but it has some huge limitations like not being able to write to DVD media. I am sure this limitation is due to almost nobody having a DVD writer when Windows XP was released back in 2001. IMAPIv2 allows you to write to CD, DVD, and even Blu-ray media, as well as read and write ISO files. IMAPIv2.0 had a problem since it was only available with Windows Vista. But in June of 2007, Microsoft released update packages for Windows XP and Windows 2003. You can download the updateshere.
To use the features added in the Windows Feature Pack for Storage 1.0, you need to download the updatehere. Windows Vista Service Pack 2 already includes the update.
I wrote this article as a sequel to my C++ article,Burning CD/DVD Media with the Image Mastering API Version 2.0. Most of the IMAPI2 samples seem to be in scripting languages. The only C# sample I found was theIBurn console project that came with the Windows Vista SDK, and more recently, the article:How to Create Optical File Images using IMAPIv2.0 by dmihailescu, which shows you how to create an ISO file.
This article was more difficult than I had thought it would be. Normally, .NET applications are supposed to be easier, but there were a number of issues that I needed to figure out to get this to work. If you're not interested in hearing me rant and rave, you can skip over the next section.
The Problems
IMAPI2 was implemented using two separate COM DLLs:imapi2.dll andimapi2fs.dll.imapi2.dll handles most of the device and recording APIs, andimapi2fs.dll handles all of the file system andIStream
APIs. This may not seem like much of a problem, especially if you are using C++. This does become a huge problem with .NET, because you need to take anIStream
created from IMAPI2FS and use it in IMAPI2 to write to the media. You end up getting an error message, something like this:
Unable to cast object of type 'IMAPI2FS.FsiStreamClass' to type 'IMAPI2.IStream'
Microsoft realized this problem, and created a project calledIBurn that they released in the Windows Vista SDK. They created an Interop namespace that combined many classes, enums, and interfaces of IMAPI2 and IMAPI2FS into one namespace in a fileInterop.cs. This fixed the problem of not being able to cast anIStream
from IMAPI2FS to IMAPI2.
Unfortunately, their implementation did not completely fix my problems. I was having other COM problems, like having the application throw exceptions whenever I would try to use any method that returned anArray
. I later discovered that this problem could have been fixed by changing theArray
toobject[]
. I was also having null reference exceptions when trying to burn multiple CDs. Microsoft admits thatInterop.cs is not a complete solution and is only used to demonstrate how to use IMAPI2 using C#.
So, off I went on my journey to create a complete solution.
I created two .NET Interop assemblies from the COM DLLs by using theMicrosoft Type Library to Assembly Converter,tlbimp.exe. I created the two assemblies by using the following commands:
tlbimp imapi2.dll /out:imapi2int.dlltlbimp imapi2fs.dll /out:imapi2fsint.dll
Once I had the .NET assemblies, I usedReflector to disassemble the interops and create C# source code. I combined the two files and made many modifications to the interfaces and helper classes, and added support for all interfaces of IMAPI2. That sounds a lot easier than it really was.
One of the biggest issues I had that seemed to take forever to figure out was the reversing of theget
,set
properties. The auto-generated code from Reflector always placed theget
before theset
. Many properties in IMAPI2 require that the properties be defined withset
beforeget
. If they are not in the proper order, then it causes the application to crash and instantly exit. It doesn't throw an exception to give you any kind of clue as to what the problem might be. It just kills the app. So, after I finally figured out what was wrong, I opened up the COM DLLs with the OLE/COM Object Viewer and went through the actual TypeLib for every property and made sure they were in the correct order.
I also chose not to implement theAStream
interface as they did inInterop.cs. I use theSystem.Runtime.InteropServices.ComTypes.IStream
Interface directly.
Lastly, in order to receive notifications for all events, I had to go through the SDK and find all of the Dispatch IDs for all of the events. Without these values, the event handlers are unable to receive notifications.
New IMAPI2 Interop - Imapi2interop.cs
My replacement forInterop.cs isImapi2Interop.cs, included in the source code. It defines the following classes and interfaces:
IBootOptions
- Specifies the boot image to add to the optical discIEnumFsiItems
- Enumerates the child directory and file items for anFsiDirectoryItem
objectIEnumProgressItems
- Enumerates a collection of progress itemsIFileSystemImageResult
- Gets information about the burn image, the image data stream, and progress informationIFsiDirectoryItem
- Adds items to or removes items from the file-system imageIFsiFileItem
- Identifies the file size and data stream of the file contentsIDiscFormat2Data
- Writes a data stream to a discIDiscFormat2DataEventArgs
- Retrieves information about the current write operationIDiscFormat2Erase
- Erases data from a discIDiscFormat2RawCD
- Writes raw images to a disc device using Disc At Once (DAO) modeIDiscFormat2RawCDEventArgs
- Retrieves information about the current write operationIDiscFormat2TrackAtOnce
- Writes audio to blank CD-R or CD-RW media in Track-At-Once modeIDiscFormat2TrackAtOnceEventArgs
- Retrieves information about the current write operationIDiscMaster2
- Enumerates the CD and DVD devices installed on the computerIDiscRecorder2
- Represents a physical deviceIDiscRecorder2Ex
- Retrieves information not available through theIDiscRecorder2
interfaceIProgressItem
- Retrieves block information for one segment of the result file imageIProgressItems
- Enumerates the progress items in a result imageIWriteEngine2
- Writes a data stream to a deviceIWriteEngine2EventArgs
- Retrieves information about the current write operationIWriteSpeedDescriptor
- Retrieves detailed write configurations supported by the disc recorder and current media
The Windows Feature Pack for Storage 1.0 also adds the following Interfaces:
IBurnVerification
- Sets the verification level of the burn operationIFileSystemImage
- Builds, imports, and exports a file system imageIFileSystemImage2
- Extends theIFileSystemImage
interface by writing multiple boot entries or boot images required for the EFI/UEFI supportIFileSystemImage3
- Extends theIFileSystemImage2
interface by setting or checking the metadata and metadata mirror files in a UDF file system (rev 2.50 and later) to determine redundancyIFsiNamedStreams
- Enumerates the named streams associated with a file in a file system imageIIsoImageManager
- Verifies if an existing ISO file contains a valid image for burningIMultiSession
- Base interface containing the properties common to the derived multi-session interfaces.IMultiSessionSequential
- Extends theIMultiSession
interface by retrieving information about the previous import session on a sequentially recorded mediaIRawCDImageCreator
- Creates a raw CD image for writing in Disc-at-once mode.IRawCDImageTrackInfo
- Tracks per-track properties that are applied to CD Media
It also defines the following events:
DDiscFormat2DataEvents
DiscFormat2Data_EventHandler
DDiscFormat2EraseEvents
DiscFormat2Erase_EventHandler
DDiscFormat2RawCDEvents
DiscFormat2RawCD_UpdateEventHandler
DDiscFormat2TrackAtOnceEvents
DiscFormat2TrackAtOnce_EventHandler
DDiscMaster2Events
DiscMaster2_NotifyDeviceAddedEventHandler
DiscMaster2_NotifyDeviceRemovedEventHandler
DFileSystemImageEvents
DFileSystemImage_EventHandler
DWriteEngine2Events
DWriteEngine2_EventHandler
Using the Code
Make sure that XP and 2003 have the IMAPI2 updates mentioned at the top of the article.
Do not add theimapi2.dll andimapi2fs.dll COM DLLs to your project. That will cause the problems listed above.
Add the fileimapi2interop.cs to your project and define the namespace in your app:
using IMAPI2.Interop;
In order to receive notification to your event handler from COM, you need to open up the fileAssemblyInfo.cs and change theComVisible
attribute totrue
:
[assembly: ComVisible(true)]
Determining the Media Type
To determine the media type and the available space on the hard drive, you create aMsftDiscFormat2Data
object and set the current recorder in theRecorder
property. You can then get the media type from theIDiscFormat2Data CurrentPhysicalMediaType
property.
Once you have the media type, create aMsftFileSystemImage
object and call theChooseImageDefaultsForMediaType
method with the media type.
To determine if any sessions have already been recorded on the media, check theIDiscFormatData2 MediaHeuristicallyBlank
property.
If it isfalse
, then other sessions have been recorded and you need to set theMsftFileSystemImage
'sMultisessionInterfaces
property with theIDiscFormat2Data MultisessionInterfaces
property, then call theIDiscFormat2Data ImportFileSystem()
method.
Then, get the free media blocks by multiplying theMsftFileSystemImage
'sFreeMediaBlocks
with the sector size (2048). If there were previous sessions recorded on the media, that space will be subtracted from the total size of the media.
privatevoid buttonDetectMedia_Click(object sender, EventArgs e){if (devicesComboBox.SelectedIndex == -1) {return; }var discRecorder = (IDiscRecorder2)devicesComboBox.Items[devicesComboBox.SelectedIndex]; MsftFileSystemImage fileSystemImage =null; MsftDiscFormat2Data discFormatData =null;try {//// Create and initialize the IDiscFormat2Data// discFormatData =new MsftDiscFormat2Data();if (!discFormatData.IsCurrentMediaSupported(discRecorder)) { labelMediaType.Text ="Media not supported!"; _totalDiscSize =0;return; }else {//// Get the media type in the recorder// discFormatData.Recorder = discRecorder; IMAPI_MEDIA_PHYSICAL_TYPE mediaType = discFormatData.CurrentPhysicalMediaType; labelMediaType.Text = GetMediaTypeString(mediaType);//// Create a file system and select the media type// fileSystemImage =new MsftFileSystemImage(); fileSystemImage.ChooseImageDefaultsForMediaType(mediaType);//// See if there are other recorded sessions on the disc//if (!discFormatData.MediaHeuristicallyBlank) { fileSystemImage.MultisessionInterfaces = discFormatData.MultisessionInterfaces; fileSystemImage.ImportFileSystem(); }Int64 freeMediaBlocks = fileSystemImage.FreeMediaBlocks; _totalDiscSize =2048 * freeMediaBlocks; } }catch (COMException exception) { MessageBox.Show(this, exception.Message,"Detect Media Error", MessageBoxButtons.OK, MessageBoxIcon.Error); }finally {if (discFormatData !=null) { Marshal.ReleaseComObject(discFormatData); }if (fileSystemImage !=null) { Marshal.ReleaseComObject(fileSystemImage); } } UpdateCapacity();}
Adding Files and Directories to the Listbox
I created a generic interface calledIMediaItem
.IMediaItem
contains three properties and one method:
interface IMediaItem{///<summary>/// Returns the full path of the file or directory///</summary>string Path {get; }///<summary>/// Returns the size of the file or directory to the next largest sector///</summary>Int64 SizeOnDisc {get; }///<summary>/// Returns the Icon of the file or directory///</summary> System.Drawing.Image FileIconImage {get; }// Adds the file or directory to the directory item, usually the root.bool AddToFileSystem(IFsiDirectoryItem rootItem);}
For file items, I created theFileItem
class. This class basically creates anIStream
by PInvoking theSHCreateStreamOnFile
Windows API call, and adds it toIFsiDirectoryItem
.
For directory items, I created theDirectoryItem
class. This uses a much simpler technique of calling theIFsiDirectoryItem.AddTree
method to add the directory and all subdirectories to theIStream
.
These classes and interfaces are located in theIMediaItem.cs file.
Creating the Image
I use theCreateMediaFileSystem
method in theMainForm
class to create theIStream
image to write to the media. I enumerate through the files and directories that were added to the filelistbox
.
privatebool CreateMediaFileSystem(IDiscRecorder2 discRecorder,out IStream dataStream){ MsftFileSystemImage fileSystemImage =null;try { fileSystemImage =new MsftFileSystemImage(); fileSystemImage.ChooseImageDefaults(discRecorder); fileSystemImage.FileSystemsToCreate = FsiFileSystems.FsiFileSystemJoliet | FsiFileSystems.FsiFileSystemISO9660; fileSystemImage.VolumeName = textBoxLabel.Text; fileSystemImage.Update +=new DFileSystemImage_EventHandler(fileSystemImage_Update);//// If multisessions, then import previous sessions//if (multisessionInterfaces !=null) { fileSystemImage.MultisessionInterfaces = multisessionInterfaces; fileSystemImage.ImportFileSystem(); }//// Get the image root// IFsiDirectoryItem rootItem = fileSystemImage.Root;//// Add Files and Directories to File System Image//foreach (IMediaItem mediaItemin listBoxFiles.Items) {//// Check if we've cancelled//if (backgroundBurnWorker.CancellationPending) {break; }//// Add to File System// mediaItem.AddToFileSystem(rootItem); } fileSystemImage.Update -=new DFileSystemImage_EventHandler(fileSystemImage_Update);//// did we cancel?//if (backgroundBurnWorker.CancellationPending) { dataStream =null;returnfalse; } dataStream = fileSystemImage.CreateResultImage().ImageStream; }catch (COMException exception) { MessageBox.Show(this, exception.Message,"Create File System Error", MessageBoxButtons.OK, MessageBoxIcon.Error); dataStream =null;returnfalse; }finally {if (fileSystemImage !=null) { Marshal.ReleaseComObject(fileSystemImage); } }returntrue;}
Multithreading
Burning or formatting media can take some time, so we do not want to perform these actions on the main UI thread. I use theBackgroundWorker
class to handle the multithreading of these lengthy tasks. TheBackgroundWorker
class allows you to set values within the thread and then call theReportProgress
method which fires aProgressChanged
event in the calling thread. When you are finished with your worker thread, it fires theRunWorkerCompleted
event to notify the calling thread that it is finished. I won't go into much detail on the whole threading process since that is not the main topic of this article.
Writing Data to Media
I write the data in theMainForm
'sbackgroundBurnWorker_DoWork
which is theDoWork
event for theBackgroundWorker backgroundBurnWorker
.
privatevoid backgroundBurnWorker_DoWork(object sender, DoWorkEventArgs e){ MsftDiscRecorder2 discRecorder =null; MsftDiscFormat2Data discFormatData =null;try {//// Create and initialize the IDiscRecorder2 object// discRecorder =new MsftDiscRecorder2();var burnData = (BurnData)e.Argument; discRecorder.InitializeDiscRecorder(burnData.uniqueRecorderId);//// Create and initialize the IDiscFormat2Data// discFormatData =new MsftDiscFormat2Data { Recorder = discRecorder, ClientName = ClientName, ForceMediaToBeClosed = _closeMedia };//// Set the verification level//var burnVerification = (IBurnVerification)discFormatData; burnVerification.BurnVerificationLevel = _verificationLevel;//// Check if media is blank, (for RW media)//object[] multisessionInterfaces =null;if (!discFormatData.MediaHeuristicallyBlank) { multisessionInterfaces = discFormatData.MultisessionInterfaces; }//// Create the file system// IStream fileSystem;if (!CreateMediaFileSystem(discRecorder, multisessionInterfaces,out fileSystem)) { e.Result = -1;return; }//// add the Update event handler// discFormatData.Update += discFormatData_Update;//// Write the data here//try { discFormatData.Write(fileSystem); e.Result =0; }catch (COMException ex) { e.Result = ex.ErrorCode; MessageBox.Show(ex.Message,"IDiscFormat2Data.Write failed", MessageBoxButtons.OK, MessageBoxIcon.Stop); }finally {if (fileSystem !=null) { Marshal.FinalReleaseComObject(fileSystem); } }//// remove the Update event handler// discFormatData.Update -= discFormatData_Update;if (_ejectMedia) { discRecorder.EjectMedia(); } }catch (COMException exception) {//// If anything happens during the format, show the message// MessageBox.Show(exception.Message); e.Result = exception.ErrorCode; }finally {if (discRecorder !=null) { Marshal.ReleaseComObject(discRecorder); }if (discFormatData !=null) { Marshal.ReleaseComObject(discFormatData); } }}
Progress Update Events
TheIDiscFormat2Data
supports cancelling with theCancelWrite
method. When I receive theUpdate
Event fromIDiscFormatData2
, I check to see if the user pressed the Cancel button. If the user has cancelled, theBackgroundWorker
'sCancellationPending
property will betrue
, and I cancel the write operation and immediately return. Otherwise, I collect the data from theIDiscFormat2DataEventArgs
object, then call thebackgroundBurnWorker.ReportProgress
so the UI thread can update the data and progress bar.
void discFormatData_Update([In, MarshalAs(UnmanagedType.IDispatch)]object sender, [In, MarshalAs(UnmanagedType.IDispatch)] objectprogress){//// Check if we've cancelled//if (backgroundBurnWorker.CancellationPending) {var format2Data = (IDiscFormat2Data)sender; format2Data.CancelWrite();return; }var eventArgs = (IDiscFormat2DataEventArgs)progress; _burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;// IDiscFormat2DataEventArgs Interface _burnData.elapsedTime = eventArgs.ElapsedTime; _burnData.remainingTime = eventArgs.RemainingTime; _burnData.totalTime = eventArgs.TotalTime;// IWriteEngine2EventArgs Interface _burnData.currentAction = eventArgs.CurrentAction; _burnData.startLba = eventArgs.StartLba; _burnData.sectorCount = eventArgs.SectorCount; _burnData.lastReadLba = eventArgs.LastReadLba; _burnData.lastWrittenLba = eventArgs.LastWrittenLba; _burnData.totalSystemBuffer = eventArgs.TotalSystemBuffer; _burnData.usedSystemBuffer = eventArgs.UsedSystemBuffer; _burnData.freeSystemBuffer = eventArgs.FreeSystemBuffer;//// Report back to the UI// backgroundBurnWorker.ReportProgress(0, _burnData);}
Formatting/Erasing RW Discs

I format the disc in theMainForm
'sbackgroundFormatWorker_DoWork
which is theDoWork
event for theBackgroundWorker backgroundFormatWorker
.
privatevoid backgroundFormatWorker_DoWork(object sender, DoWorkEventArgs e){ MsftDiscRecorder2 discRecorder =null; MsftDiscFormat2Erase discFormatErase =null;try {//// Create and initialize the IDiscRecorder2// discRecorder =new MsftDiscRecorder2();var activeDiscRecorder = (string)e.Argument; discRecorder.InitializeDiscRecorder(activeDiscRecorder);//// Create the IDiscFormat2Erase and set properties// discFormatErase =new MsftDiscFormat2Erase { Recorder = discRecorder, ClientName = ClientName, FullErase = !checkBoxQuickFormat.Checked };//// Setup the Update progress event handler// discFormatErase.Update += discFormatErase_Update;//// Erase the media here//try { discFormatErase.EraseMedia(); e.Result =0; }catch (COMException ex) { e.Result = ex.ErrorCode; MessageBox.Show(ex.Message,"IDiscFormat2.EraseMedia failed", MessageBoxButtons.OK, MessageBoxIcon.Stop); }//// Remove the Update progress event handler// discFormatErase.Update -= discFormatErase_Update;//// Eject the media//if (checkBoxEjectFormat.Checked) { discRecorder.EjectMedia(); } }catch (COMException exception) {//// If anything happens during the format, show the message// MessageBox.Show(exception.Message); }finally {if (discRecorder !=null) { Marshal.ReleaseComObject(discRecorder); }if (discFormatErase !=null) { Marshal.ReleaseComObject(discFormatErase); } }}
History
- March 21, 2008
- March 25, 2008
- Added the Visual Studio 2005 project
- March 29, 2008
- Detects media type and size, supports multi-session
- May 2, 2009
- Added support for Windows Feature Pack for Storage, and bug fixes
- December 13, 2009
- Removed calls to the
AcquireExclusiveAccess
andReleaseExclusiveAccess
methods as they are not needed - Calls
IDiscRecorder2.SupportedProfiles
to enumerate supported disk types
- March 2, 2010
- Fixed some incorrect method parameters in the
IDiscRecorder2Ex
class - Fixed a potential threading issue
- Added an icon
- March 22, 2010
- Fixed a very bad Windows XP bug found by Leroe where the
MsftFileSystemImageClass
was always unnecessarily using theIFileSystemImage3
Interface when just using theIFileSystemImage
interface would suffice. Also did a little code refactoring.