使用 C# 和 IMAPI2 刻录和擦除 CD/DVD/蓝光介质






4.96/5 (196投票s)
在 C# 中使用 Image Mastering API。

引言
Windows 在发布 Vista 操作系统时引入了新的 IMAPIv2.0,相比原始 IMAPI 有了很大的改进。原始 IMAPI 对 CDROM 非常好,但存在一些巨大限制,例如无法写入 DVD 介质。我相信这个限制是因为在 2001 年 Windows XP 发布时,几乎没有人拥有 DVD 刻录机。IMAPIv2 允许您写入 CD、DVD 甚至蓝光介质,以及读取和写入 ISO 文件。IMAPIv2.0 存在一个问题,因为它仅随 Windows Vista 提供。但在 2007 年 6 月,微软发布了适用于 Windows XP 和 Windows 2003 的更新程序包。您可以在 此处 下载更新。
要使用 Windows 存储功能包 1.0 中添加的功能,您需要在此处 下载更新。Windows Vista Service Pack 2 已包含此更新。
我撰写本文是作为我 C++ 文章《使用 Image Mastering API 版本 2.0 刻录 CD/DVD 介质》的续篇。大多数 IMAPI2 示例似乎都是用脚本语言编写的。我找到的唯一 C# 示例是 Windows Vista SDK 随附的 **IBurn** 控制台项目,以及最近 dmihailescu 的文章:《如何使用 IMAPIv2.0 创建光盘映像文件》,该文章展示了如何创建 ISO 文件。
这篇文章比我想象的要困难。通常,.NET 应用程序应该更容易,但我需要弄清楚一些问题才能使其正常工作。如果您不想听我抱怨,可以跳过下一节。
问题
IMAPI2 是通过两个独立的 COM DLL 实现的:imapi2.dll 和 imapi2fs.dll。imapi2.dll 处理大多数设备和录制 API,而 imapi2fs.dll 处理所有文件系统和 IStream
API。这可能看起来不是什么大问题,尤其是在使用 C++ 时。但对于 .NET 来说,这会成为一个巨大的问题,因为您需要将 IMAPI2FS 创建的 IStream
用于 IMAPI2 以写入介质。您最终会收到一个类似以下的错误消息:
Unable to cast object of type 'IMAPI2FS.FsiStreamClass' to type 'IMAPI2.IStream'
微软意识到了这个问题,并创建了一个名为 **IBurn** 的项目,并在 Windows Vista SDK 中发布了它。他们创建了一个 Interop 命名空间,将 IMAPI2 和 IMAPI2FS 的许多类、枚举和接口合并到一个命名空间中,位于一个名为 Interop.cs 的文件中。这解决了将 IMAPI2FS 的 IStream
无法强制转换为 IMAPI2 的问题。
不幸的是,他们的实现并没有完全解决我的问题。我遇到了其他 COM 问题,例如当我尝试使用返回 Array
的任何方法时,应用程序都会抛出异常。后来我发现,通过将 Array
更改为 object[]
可以解决此问题。当我尝试刻录多个 CD 时,我还遇到了空引用异常。微软承认 Interop.cs 并不是一个完整的解决方案,仅用于演示如何使用 C# 来使用 IMAPI2。
于是,我踏上了创建完整解决方案的征程。
我使用 **Microsoft Type Library to Assembly Converter**(tlbimp.exe)从 COM DLL 创建了两个 .NET Interop 程序集。我使用了以下命令创建了这两个程序集:
tlbimp imapi2.dll /out:imapi2int.dll
tlbimp imapi2fs.dll /out:imapi2fsint.dll
获得 .NET 程序集后,我使用 Reflector 反编译了互操作程序集,并创建了 C# 源代码。我合并了这两个文件,并对接口和辅助类进行了许多修改,并增加了对 IMAPI2 所有接口的支持。这听起来比实际操作简单得多。
我遇到的一个最大的问题,花了好长时间才弄明白,就是 get
和 set
属性的顺序颠倒。Reflector 自动生成的代码总是将 get
放在 set
之前。IMAPI2 中的许多属性要求属性定义时 set
必须在 get
之前。如果顺序不正确,应用程序就会崩溃并立即退出。它不会抛出异常来提供任何关于问题的线索。它只会终止应用程序。所以,在我终于弄清楚问题所在后,我使用 OLE/COM 对象查看器打开了 COM DLL,并检查了每个属性的实际 TypeLib,确保它们的顺序正确。
我还选择不实现 Interop.cs 中的 AStream
接口。我直接使用 System.Runtime.InteropServices.ComTypes.IStream
接口。
最后,为了接收所有事件的通知,我不得不查阅 SDK,找到所有事件的 Dispatch ID。没有这些值,事件处理程序就无法接收通知。
新的 IMAPI2 互操作 - Imapi2interop.cs
我用来替换 Interop.cs 的是 Imapi2Interop.cs,包含在源代码中。它定义了以下类和接口:
IBootOptions
- 指定要添加到光盘的启动映像IEnumFsiItems
- 枚举FsiDirectoryItem
对象下的子目录和文件项IEnumProgressItems
- 枚举进度项集合IFileSystemImageResult
- 获取有关刻录映像、映像数据流和进度信息IFsiDirectoryItem
- 将项添加到文件系统映像或从中移除项IFsiFileItem
- 标识文件内容的文件大小和数据流IDiscFormat2Data
- 将数据流写入光盘IDiscFormat2DataEventArgs
- 检索有关当前写入操作的信息IDiscFormat2Erase
- 从光盘擦除数据IDiscFormat2RawCD
- 使用 Disc At Once (DAO) 模式将原始映像写入光盘设备IDiscFormat2RawCDEventArgs
- 检索有关当前写入操作的信息IDiscFormat2TrackAtOnce
- 以 Track-At-Once 模式将音频写入空白 CD-R 或 CD-RW 介质IDiscFormat2TrackAtOnceEventArgs
- 检索有关当前写入操作的信息IDiscMaster2
- 枚举计算机上安装的 CD 和 DVD 设备IDiscRecorder2
- 表示物理设备IDiscRecorder2Ex
- 检索IDiscRecorder2
接口无法提供的信息IProgressItem
- 检索结果映像某个段的块信息IProgressItems
- 枚举结果映像中的进度项IWriteEngine2
- 将数据流写入设备IWriteEngine2EventArgs
- 检索有关当前写入操作的信息IWriteSpeedDescriptor
- 检索刻录机和当前介质支持的详细写入配置
Windows 存储功能包 1.0 还添加了以下接口:
IBurnVerification
- 设置刻录操作的验证级别IFileSystemImage
- 构建、导入和导出文件系统映像IFileSystemImage2
- 通过写入多个启动条目或 EFI/UEFI 支持所需的启动映像来扩展IFileSystemImage
接口IFileSystemImage3
- 通过设置或检查 UDF 文件系统(2.50 及更高版本)的元数据和元数据镜像文件来确定冗余,从而扩展IFileSystemImage2
接口IFsiNamedStreams
- 枚举文件系统映像中与文件关联的命名流IIsoImageManager
- 验证现有 ISO 文件是否包含可供刻录的有效映像IMultiSession
- 包含派生多会话接口的通用属性的基接口。IMultiSessionSequential
- 通过检索顺序录制的介质上先前导入会话的信息来扩展IMultiSession
接口IRawCDImageCreator
- 创建原始 CD 映像以在 Disc-at-once 模式下写入。IRawCDImageTrackInfo
- 跟踪应用于 CD 介质的每轨属性
它还定义了以下事件:
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
请确保 XP 和 2003 已安装本文开头提到的 IMAPI2 更新。
不要将 imapi2.dll 和 imapi2fs.dll COM DLL 添加到您的项目中。这会导致上述问题。
将 imapi2interop.cs 文件添加到您的项目中,并在您的应用程序中定义命名空间。
using IMAPI2.Interop;
为了接收 COM 发送给事件处理程序的通知,您需要打开 AssemblyInfo.cs 文件并将 ComVisible
属性更改为 true
。
[assembly: ComVisible(true)]
确定介质类型
要确定介质类型和硬盘上的可用空间,请创建一个 MsftDiscFormat2Data
对象,并在 Recorder
属性中设置当前刻录机。然后,您可以从 IDiscFormat2Data CurrentPhysicalMediaType
属性获取介质类型。
获得介质类型后,创建一个 MsftFileSystemImage
对象,并使用介质类型调用 ChooseImageDefaultsForMediaType
方法。
要确定介质上是否已记录了任何会话,请检查 IDiscFormatData2 MediaHeuristicallyBlank
属性。
如果为 false
,则表示已记录其他会话,您需要将 MsftFileSystemImage
的 MultisessionInterfaces
属性设置为 IDiscFormat2Data MultisessionInterfaces
属性,然后调用 IDiscFormat2Data ImportFileSystem()
方法。
然后,通过将 MsftFileSystemImage
的 FreeMediaBlocks
与扇区大小(2048)相乘来获取可用的介质块。如果介质上记录了以前的会话,则该空间将从介质的总大小中扣除。
private void 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();
}
将文件和目录添加到列表框
我创建了一个名为 IMediaItem
的通用接口。IMediaItem
包含三个属性和一个方法:
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);
}
对于文件项,我创建了 FileItem
类。此类通过 PInvoke 调用 SHCreateStreamOnFile
Windows API 来创建一个 IStream
,并将其添加到 IFsiDirectoryItem
。
对于目录项,我创建了 DirectoryItem
类。此类使用一种更简单的方法,通过调用 IFsiDirectoryItem.AddTree
方法将目录及其所有子目录添加到 IStream
。
这些类和接口位于 IMediaItem.cs 文件中。
创建映像
我在 MainForm
类中使用 CreateMediaFileSystem
方法来创建要写入介质的 IStream
映像。我遍历添加到文件 listbox
的文件和目录。
private bool 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 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;
}
catch (COMException exception)
{
MessageBox.Show(this, exception.Message,
"Create File System Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
dataStream = null;
return false;
}
finally
{
if (fileSystemImage != null)
{
Marshal.ReleaseComObject(fileSystemImage);
}
}
return true;
}
多线程
刻录或格式化介质可能需要一些时间,因此我们不希望在主 UI 线程上执行这些操作。我使用 BackgroundWorker
类来处理这些耗时任务的多线程。BackgroundWorker
类允许您在线程内设置值,然后调用 ReportProgress
方法,该方法会在调用线程中触发 ProgressChanged
事件。当您的工作线程完成后,它会触发 RunWorkerCompleted
事件,通知调用线程已完成。我不会详细介绍整个线程过程,因为这不是本文的主要内容。
将数据写入介质
我在 MainForm
的 backgroundBurnWorker_DoWork
中写入数据,这是 BackgroundWorker backgroundBurnWorker
的 DoWork
事件。
private void 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);
}
}
}
进度更新事件
IDiscFormat2Data
支持通过 CancelWrite
方法取消操作。当我收到 IDiscFormatData2
的 Update
事件时,我检查用户是否按下了“取消”按钮。如果用户已取消,则 BackgroundWorker
的 CancellationPending
属性将为 true
,我将取消写入操作并立即返回。否则,我收集 IDiscFormat2DataEventArgs
对象中的数据,然后调用 backgroundBurnWorker.ReportProgress
,以便 UI 线程可以更新数据和进度条。
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);
}
格式化/擦除 RW 光盘
我在 MainForm
的 backgroundFormatWorker_DoWork
中格式化光盘,这是 BackgroundWorker backgroundFormatWorker
的 DoWork
事件。
private void 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);
}
}
}
历史
- 2008年3月21日
- 首次发布
- 2008年3月25日
- 添加了 Visual Studio 2005 项目
- 2008年3月29日
- 检测介质类型和大小,支持多会话
- 2009年5月2日
- 增加了对 Windows 存储功能包的支持,并进行了错误修复
- 2009年12月13日
- 移除了对
AcquireExclusiveAccess
和ReleaseExclusiveAccess
方法的调用,因为它们不再需要 - 调用
IDiscRecorder2.SupportedProfiles
来枚举支持的光盘类型 - 2010年3月2日
- 修复了
IDiscRecorder2Ex
类中一些不正确的参数 - 修复了潜在的线程问题
- 添加了图标
- 2010年3月22日
- 修复了 Leroe 发现的一个非常严重的 Windows XP 错误,即
MsftFileSystemImageClass
总是无必要地使用IFileSystemImage3
接口,而使用IFileSystemImage
接口就足够了。还进行了一些代码重构。