5,445,109 members and growing! (14,910 online)
Email Password   helpLost your password?
Desktop Development » Miscellaneous » General     Intermediate License: The Code Project Open License (CPOL)

Burning and Erasing CD/DVD/Blu-ray Media with C# and IMAPI2

By Eric Haddan

Using the Image Mastering API in C#
C# (C# 3.0, C# 2.0, C#), Windows (Windows, WinXP, Win2003, Vista), .NET (.NET, .NET 3.0, .NET 2.0), Win32, COM, Dev

Posted: 21 Mar 2008
Updated: 1 Apr 2008
Views: 39,803
Bookmarked: 108 times
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
Prize winner in Competition "Best C# article of March 2008"
59 votes for this Article.
Popularity: 8.32 Rating: 4.70 out of 5
3 votes, 5.2%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
5 votes, 8.6%
4
50 votes, 86.2%
5

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 updates here.

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 the IBurn 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 and imapi2fs.dll. imapi2.dll handles most of the device and recording APIs and imapi2fs.dll handles all of the file system and IStream 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 an IStream 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 called IBurn 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 file Interop.cs. This fixed the problem of not being able to cast an IStream 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 an Array. I later discovered that this problem could have been fixed by changing the Array to object[]. I was also having null reference exceptions when trying to burn multiple CDs. Microsoft admits that Interop.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 the Microsoft Type Library to Assembly Converter tlbimp.exe. I created the two assemblies by using the following commands:

tlbimp imapi2.dll /out:imapi2int.dll
tlbimp imapi2fs.dll /out:imapi2fsint.dll

Once I had the .NET assemblies, I used the Lutz Reflector 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 the get, set properties. The auto-generated code from Reflector always placed the get before the set. Many properties in IMAPI2 require that the properties be defined with set before get. 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 the AStream interface as they did in Interop.cs. I use the System.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 for Interop.cs is Imapi2Interop.cs included in the source code. It defines the following classes and interfaces:

  • IBootOptions - Specify the boot image to add to the optical disc
  • IEnumFsiItems - Enumerate the child directory and file items for an FsiDirectoryItem object
  • IEnumProgressItems - Enumerate a collection of progress items
  • IFileSystemImageResult - Get information about the burn image, the image data stream, and progress information
  • IFsiDirectoryItem - Add items to or remove items from the file-system image
  • IFsiFileItem - Identify the file size and data stream of the file contents
  • IDiscFormat2Data - Write a data stream to a disc
  • IDiscFormat2DataEventArgs - Retrieve information about the current write operation
  • IDiscFormat2Erase - Erase data from a disc
  • IDiscFormat2RawCD - Write raw images to a disc device using Disc At Once (DAO) mode
  • IDiscFormat2RawCDEventArgs - Retrieve information about the current write operation
  • IDiscFormat2TrackAtOnce - Write audio to blank CD-R or CD-RW media in Track-At-Once mode
  • IDiscFormat2TrackAtOnceEventArgs - Retrieve information about the current write operation
  • IDiscMaster2 - Enumerate the CD and DVD devices installed on the computer
  • IDiscRecorder2 - Represents a physical device
  • IDiscRecorder2Ex - Retrieves information not available through IDiscRecorder2 interface
  • IProgressItem - Retrieve block information for one segment of the result file image
  • IProgressItems - Enumerate the progress items in a result image
  • IWriteEngine2 - Write a data stream to a device
  • IWriteEngine2EventArgs - Retrieve information about the current write operation
  • IWriteSpeedDescriptor - Retrieve detailed write configurations supported by the disc recorder and current 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 the imapi2.dll and imapi2fs.dll COM DLLs to your project. That will cause the problems listed above.

Add the file imapi2interop.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 file AssemblyInfo.cs and change the ComVisible attribute to true:

[assembly: ComVisible(true)]

Determining Media Type

To determine the media type and available space on the hard drive, you create a MsftDiscFormat2Data object and set the current recorder in the Recorder property. You can then get the media type from the IDiscFormat2Data CurrentPhysicalMediaType property.

Once you have the media type, create a MsftFileSystemImage object and call the ChooseImageDefaultsForMediaType method with the media type.

To determine if any sessions have already been recorded on the media, check the IDiscFormatData2 MediaHeuristicallyBlank property.

If it is false, then other sessions have been recorded and you need to set the MsftFileSystemImage's MultisessionInterfaces property with the IDiscFormat2Data MultisessionInterfaces property, then call the IDiscFormat2Data ImportFileSystem() method.

Then, get the free media blocks by multiplying the MsftFileSystemImage's FreeMediaBlocks 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.

private void buttonDetectMedia_Click(object sender, EventArgs e)
{
    IDiscRecorder2 discRecorder =
    (IDiscRecorder2)devicesComboBox.Items[devicesComboBox.SelectedIndex];

    //
    // Create and initialize the IDiscFormat2Data
    //
    MsftDiscFormat2Data 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
        //
        MsftFileSystemImage 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();
        }

        int freeMediaBlocks = fileSystemImage.FreeMediaBlocks;
        totalDiscSize = 2048 * freeMediaBlocks;
    }

    UpdateCapacity();
}

Adding Files and Directories to the Listbox

I created a generic interface called IMediaItem. 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.Icon FileIcon { get; }

    /// <summary>
    /// Adds the file or directory to the directory item, usually the root.
    /// </summary>
    bool AddToFileSystem(IFsiDirectoryItem rootItem);
}

For file items, I created the FileItem class. This class basically creates an IStream by PInvoking the SHCreateStreamOnFile Windows API call and adds it to the IFsiDirectoryItem.

For directory items, I created the DirectoryItem class. This uses a much simpler technique of calling the IFsiDirectoryItem.AddTree method to add the directory and all subdirectories to the IStream.

These classes and interfaces are located in the IMediaItem.cs file.

Creating the Image

I use the CreateMediaFileSystem method in the MainForm class to create the IStream image to write to the media. I enumerate through the files and directories that were added to the file listbox.

private bool CreateMediaFileSystem(IDiscRecorder2 discRecorder, out IStream dataStream)
{
    MsftFileSystemImage fileSystemImage = new MsftFileSystemImage();
    fileSystemImage.ChooseImageDefaults(discRecorder);
    fileSystemImage.FileSystemsToCreate =
        FsiFileSystems.FsiFileSystemJoliet | FsiFileSystems.FsiFileSystemISO9660;
    fileSystemImage.VolumeName = textBoxLabel.Text;
    fileSystemImage.Update += new DFileSystemImage_EventHandler(fileSystemImage_Update);
    //
    // Get the image root
    //
    IFsiDirectoryItem rootItem = fileSystemImage.Root;
    //
    // Add Files and Directories to File System Image
    //
    foreach (IMediaItem mediaItem in 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;
        return false;
    }
    dataStream = fileSystemImage.CreateResultImage().ImageStream;
    return true;
}

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 the BackgroundWorker class to handle the multithreading of these lengthy tasks. The BackgroundWorker class allows you to set values within the thread and then call the ReportProgress method which fires a ProgressChanged event in the calling thread. When you are finished with your worker thread, it fires the RunWorkerCompleted event to notify the calling thread that it is finished. I won't go into much details on the whole threading process since that is not the main topic of this article.

Writing Data to Media

I write the data in the MainForm's backgroundBurnWorker_DoWork which is the DoWork event for the BackgroundWorker backgroundBurnWorker.

private void backgroundBurnWorker_DoWork(object sender, DoWorkEventArgs e)
{
    //
    // Create and initialize the IDiscRecorder2 object
    //
    MsftDiscRecorder2 discRecorder = new MsftDiscRecorder2();
    BurnData burnData = (BurnData)e.Argument;
    discRecorder.InitializeDiscRecorder(burnData.uniqueRecorderId);
    discRecorder.AcquireExclusiveAccess(true, m_clientName);
    //
    // Create the file system
    //
    IStream fileSystem = null;
    if (!CreateMediaFileSystem(discRecorder, out fileSystem))
    {
        e.Result = -1;
        return;
    }
    //
    // Create and initialize the IDiscFormat2Data
    //
    MsftDiscFormat2Data discFormatData = new MsftDiscFormat2Data();
    discFormatData.Recorder = discRecorder;
    discFormatData.ClientName = m_clientName;
    discFormatData.ForceMediaToBeClosed = checkBoxCloseMedia.Checked;
    //
    // add the Update event handler
    //
    discFormatData.Update += new DiscFormat2Data_EventHandler(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);
    }
    //
    // remove the Update event handler
    //
    discFormatData.Update -= new DiscFormat2Data_EventHandler(discFormatData_Update);
    if (this.checkBoxEject.Checked)
    {
        discRecorder.EjectMedia();
    }
    discRecorder.ReleaseExclusiveAccess();
}

Progress Update Events

The IDiscFormat2Data supports cancelling with the CancelWrite method. When I receive the Update Event from the IDiscFormatData2, I check to see if the user pressed the cancel button. If they have cancelled, the BackgroundWorker's CancellationPending property will be true and I cancel the write operation and immediately return. Otherwise I collect the data from the IDiscFormat2DataEventArgs object, then call the backgroundBurnWorker.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)
    {
        IDiscFormat2Data format2Data = (IDiscFormat2Data)sender;
        format2Data.CancelWrite();
        return;
    }
    IDiscFormat2DataEventArgs eventArgs = (IDiscFormat2DataEventArgs)progress;
    m_burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;
    // IDiscFormat2DataEventArgs Interface
    m_burnData.elapsedTime = eventArgs.ElapsedTime;
    m_burnData.remainingTime = eventArgs.RemainingTime;
    m_burnData.totalTime = eventArgs.TotalTime;
    // IWriteEngine2EventArgs Interface
    m_burnData.currentAction = eventArgs.CurrentAction;
    m_burnData.startLba = eventArgs.StartLba;
    m_burnData.sectorCount = eventArgs.SectorCount;
    m_burnData.lastReadLba = eventArgs.LastReadLba;
    m_burnData.lastWrittenLba = eventArgs.LastWrittenLba;
    m_burnData.totalSystemBuffer = eventArgs.TotalSystemBuffer;
    m_burnData.usedSystemBuffer = eventArgs.UsedSystemBuffer;
    m_burnData.freeSystemBuffer = eventArgs.FreeSystemBuffer;
    //
    // Report back to the UI
    //
    backgroundBurnWorker.ReportProgress(0, m_burnData);
}

Formatting/Erasing RW Discs

burnmedia_format.png

I format the disc in the MainForm's backgroundFormatWorker_DoWork which is the DoWork event for the BackgroundWorker backgroundFormatWorker.

private void backgroundFormatWorker_DoWork(object sender, DoWorkEventArgs e)
{
    //
    // Create and initialize the IDiscRecorder2
    //
    MsftDiscRecorder2 discRecorder = new MsftDiscRecorder2();
    string activeDiscRecorder = (string)e.Argument;
    discRecorder.InitializeDiscRecorder(activeDiscRecorder);
    discRecorder.AcquireExclusiveAccess(true, m_clientName);
    //
    // Create the IDiscFormat2Erase and set properties
    //
    MsftDiscFormat2Erase discFormatErase = new MsftDiscFormat2Erase();
    discFormatErase.Recorder = discRecorder;
    discFormatErase.ClientName = m_clientName;
    discFormatErase.FullErase = !checkBoxQuickFormat.Checked;
    //
    // Setup the Update progress event handler
    //
    discFormatErase.Update += new DiscFormat2Erase_EventHandler(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 -= new DiscFormat2Erase_EventHandler(discFormatErase_Update);
    if (checkBoxEjectFormat.Checked)
    {
        discRecorder.EjectMedia();
    }
    discRecorder.ReleaseExclusiveAccess();
}

History

  • March 21, 2008 - Initial release
  • March 25, 2008 - Added Visual Studio 2005 project
  • March 29, 2008 - Detects media type and size, supports multi-session

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Eric Haddan


Specialize mostly in System-level, diagnostic, disassembly, and Internet utilities.

Going way back, I started programming Basic on the Apple II Plus, developed DOS Apps using Microsoft QuickC, and developing Window Applications since Windows 3.0

Developing Software professionally for more than 15 years.

MCSD.NET in C#

The Tranxition Developer's Blog[^]

Occupation: Software Developer (Senior)
Company: Tranxition Corp
Location: United States United States

Other popular Miscellaneous articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 132 (Total in Forum: 132) (Refresh)FirstPrevNext
Subject  Author Date 
QuestionSCSI drive errormemberkevin01221:29 2 Sep '08  
QuestionSet the correct recorder if there are two writers [modified]memberKevinPorter11:12 1 Sep '08  
AnswerRe: Set the correct recorder if there are two writersmemberKevinPorter6:07 4 Sep '08  
QuestionAnother question on detecting media typemembersoccerdad17:39 30 Aug '08  
GeneralfileIcon = System.Drawing.Icon.FromHandle(shinfo.hIcon) ErrromemberUn Do5:12 28 Aug '08  
GeneralDoes XP automatically get IMAPI v2 from Windows Update?memberSerge Wautier10:34 21 Aug '08  
GeneralRe: Does XP automatically get IMAPI v2 from Windows Update?memberEric Haddan19:29 26 Aug '08  
GeneralRe: Does XP automatically get IMAPI v2 from Windows Update?memberSerge Wautier21:51 26 Aug '08  
QuestionFunctionality for Simulation and verify after burnmemberrabindrahota19:08 17 Aug '08  
AnswerRe: Functionality for Simulation and verify after burnmemberEric Haddan19:18 26 Aug '08  
GeneralError when debugmemberlalamonkey1:54 9 Aug '08  
GeneralRe: Error when debugmemberEric Haddan19:06 26 Aug '08  
GeneralISO 9660 Level 1membersatbir19:47 2 Aug '08  
GeneralRe: ISO 9660 Level 1memberEric Haddan19:02 26 Aug '08  
GeneralDVD Player Functionalitymemberjmanson10:25 30 Jul '08  
GeneralRe: DVD Player FunctionalitymemberEric Haddan14:55 30 Jul '08  
GeneralBrilliant!memberfsoldt21:25 23 Jul '08  
GeneralRe: Brilliant!memberEric Haddan14:57 30 Jul '08  
GeneralRe: Brilliant!memberfsoldt12:21 31 Jul '08  
GeneralRe: Brilliant!memberEric Haddan8:48 1 Aug '08  
GeneralOut of Memory exception when we have many subdirectoriesmemberMember 24683034:25 21 Jul '08  
GeneralRe: Out of Memory exception when we have many subdirectoriesmemberrabindrahota19:13 17 Aug '08