Implementando Gravação de CDs em C#

Recentemente, um projeto legado na empresa exigiu a adição de funcionalidade para exportar dados para CDs. Inicialmente, resisti a essa solicitação, considerando o CD um meio de armazenamento obsoleto. Assim como walkmans, MP3s e MP4s, os CDs estão prestes a sair de cena. No entanto, seguindo as instruções da gestão, precisei implementar essa funcionalidade.

Após pesquisa, descobri que a documentação disponível sobre gravação de CDs em C# é bastante antiga e os métodos existentes não são particularmente convenientes. Embora tenham encontrado exemplos úteis, como o do CodePlex Archive e o projeto BurnCdDemo, enfrentamos desafios durante a implementação, especialmente ao lidar com permissões de administrador e problemas de compatibilidade.

Neste artigo, apresentaremos uma abordagem simplificada para implementar funcionalidades básicas de gravação de CDs: listagem de unidades ópticas, obtenção de informações do disco, adição de arquivos, processo de gravação e notificação de progresso.

**Ferramentas Necessárias**

Como meu computador de trabalho não possui unidade de CD, a testagem se tornou difícil. A solução foi instalar uma unidade óptica virtual. Recomendo o PhantomBurner, uma ferramenta de virtualização confiável. Após a instalação e ativação, crie um disco virtual usando DVD+RW para facilitar os testes.

**Implementação**

**1. Listagem de Dispositivos**

Primeiro, criamos uma classe OpticalDeviceHelper com um método estático para obter a lista de unidades ópticas disponíveis.


/// <summary>
/// Obtém a lista de unidades ópticas disponíveis
/// </summary>
/// <returns>Lista de unidades ópticas</returns>
public static List<OpticalDevice> GetOpticalDeviceList()
{
    List<OpticalDevice> deviceList = new List<OpticalDevice>();

    // Cria um objeto DiscMaster2 para conectar às unidades ópticas
    MsftDiscMaster2 discMaster = new MsftDiscMaster2();
    for (int i = 0; i < discMaster.Count; i++)
    {
        if (discMaster[i] != null)
        {
            OpticalDevice device = new OpticalDevice(discMaster[i]);
            deviceList.Add(device);
        }
    }
    return deviceList;
}

A classe OpticalDevice representa uma unidade óptica com as seguintes propriedades principais:


MsftDiscRecorder2 deviceController = null;    // Controlador da unidade

/// <summary>
/// Nome da unidade
/// </summary>
public string DeviceName { get; private set; }    

/// <summary>
/// Suporte do dispositivo para gravação
/// </summary>
public bool IsDeviceSupported { get; private set; }

/// <summary>
/// Suporte do meio atual
/// </summary>
public bool IsCurrentMediaSupported { get; private set; }

/// <summary>
/// Espaço livre no disco
/// </summary>
public long FreeDiskSpace { get; private set; }

/// <summary>
/// Tamanho total do disco
/// </summary>
public long TotalDiskSpace { get; private set; }

/// <summary>
/// Estado do meio atual
/// </summary>
public IMAPI_FORMAT2_DATA_MEDIA_STATE CurrentMediaState { get; private set; }

/// <summary>
/// Nome do estado do meio
/// </summary>
public string CurrentMediaStateName { get; private set; }

/// <summary>
/// Tipo do meio físico
/// </summary>
public IMAPI_MEDIA_PHYSICAL_TYPE CurrentMediaType { get; private set; }

/// <summary>
/// Nome do tipo do meio
/// </summary>
public string CurrentMediaTypeName { get; private set; }

/// <summary>
/// Indica se é possível gravar
/// </summary>
public bool CanWrite {get;private set;}

O método de inicialização da classe OpticalDevice é responsável por configurar todas as propriedades:


/// <summary>
/// Construtor da OpticalDevice
/// </summary>
/// <param name="uniqueId"></param>Identificador único
public OpticalDevice(string uniqueId)
{
    this.uniqueId = uniqueId;
    deviceController = new MsftDiscRecorder2();
    deviceController.InitializeDiscRecorder(uniqueId);
    InitializeDevice();

    this.FilesToBurn = new List<BurnFile>();
    this.TotalFilesSize = 0;
}

/// <summary>
/// Inicializa a unidade óptica
/// Atualiza informações da unidade, pode ser chamado após inserir um novo disco
/// </summary>
public void InitializeDevice()
{
    try
    {
        if (deviceController.VolumePathNames != null && deviceController.VolumePathNames.Length > 0)
        {
            foreach (object mountPoint in deviceController.VolumePathNames)
            {   // Usa o primeiro ponto de montagem encontrado
                DeviceName = mountPoint.ToString();
                break;
            }
        }
        
        // Define o novo formato do disco e define o dispositivo
        MsftDiscFormat2Data dataWriter = new MsftDiscFormat2Data();
        dataWriter.Recorder = deviceController;

        if (!dataWriter.IsRecorderSupported(deviceController))
        {
            return;
        }
        if (!dataWriter.IsCurrentMediaSupported(deviceController))
        {
            return;
        }
        if (dataWriter.FreeSectorsOnMedia >= 0)
        {   // Espaço disponível
            FreeDiskSpace = dataWriter.FreeSectorsOnMedia * 2048L;
        }

        if (dataWriter.TotalSectorsOnMedia >= 0)
        {   // Tamanho total
            TotalDiskSpace = dataWriter.TotalSectorsOnMedia * 2048L;
        }
        
        CurrentMediaState = dataWriter.CurrentMediaStatus;  // Estado do meio
        CurrentMediaStateName = OpticalDeviceHelper.GetMediaStateName(CurrentMediaState);
        CurrentMediaType = dataWriter.CurrentPhysicalMediaType; // Tipo do meio
        CurrentMediaTypeName = OpticalDeviceHelper.GetMediaTypeName(CurrentMediaType);
        CanWrite = OpticalDeviceHelper.GetMediaBurnable(CurrentMediaState);   // Verifica se é possível gravar
    }
    catch (COMException ex)
    {
        string errMsg = ex.Message.Replace("\r\n", ""); // Remove caracteres de nova linha da mensagem de erro
        this.CurrentMediaStateName = $"Exceção COM:{errMsg}";
    }
    catch (Exception ex)
    {
        this.CurrentMediaStateName = $"{ex.Message}";
    }
}

É importante notar que operações em unidades ópticas requerem permissões de administrador. Para configurar um aplicativo C# para ser executado com privilégios de administrador, consulte a documentação apropriada.

**2. Adicionando Arquivos para Gravação**

Adicionamos à classe OpticalDevice uma lista de arquivos para gravar e o tamanho total desses arquivos:


/// <summary>
/// Lista de arquivos para gravar
/// </summary>
public List<BurnFile> FilesToBurn {get;set;}

/// <summary>
/// Tamanho total dos arquivos para gravar
/// </summary>
public long TotalFilesSize { get; set; }

A classe BurnFile é definida como:


/// <summary>
/// Representa um arquivo ou pasta para gravação
/// </summary>
public class BurnFile
{
    /// <summary>
    /// Caminho do arquivo/pasta
    /// </summary>
    public string FilePath { get; set; }

    /// <summary>
    /// Nome do arquivo/pasta
    /// </summary>
    public string FileName { get; set; }

    /// <summary>
    /// Indica se é uma pasta
    /// </summary>
    public bool IsDirectory { get; set; }

    /// <summary>
    /// Tamanho do arquivo/pasta
    /// </summary>
    public long Size { get; set; }
}

O método para adicionar arquivos à lista de gravação é:


/// <summary>
/// Adiciona um arquivo ou pasta à lista de gravação
/// </summary>
public BurnFile AddFileToBurn(string path)
{
    BurnFile file = null;
    if(string.IsNullOrEmpty(path))
    {
        throw new Exception("O caminho do arquivo não pode ser vazio.");
    }
    if(!CanWrite)
    {
        throw new Exception("O estado atual do disco não permite gravação.");
    }
    
    file = new BurnFile();
    long fileSize = 0;
    if (Directory.Exists(path))
    {
        DirectoryInfo dirInfo = new DirectoryInfo(path);
        fileSize = GetDirectorySize(path);
        file.FileName = dirInfo.Name;
        file.FilePath = dirInfo.FullName;
        file.Size = fileSize;
        file.IsDirectory = true;
    }
    else if (File.Exists(path))
    {
        FileInfo fileInfo = new FileInfo(path);
        fileSize = fileInfo.Length;
        file.FileName = fileInfo.Name;
        file.FilePath = fileInfo.FullName;
        file.Size = fileSize;
        file.IsDirectory = false;
    }
    else
    {
        throw new Exception("Arquivo ou pasta não existe");
    }
    
    if (TotalFilesSize + fileSize >= FreeDiskSpace)
    {
        throw new Exception("Espaço insuficiente no disco");
    }
    
    if (FilesToBurn.Any(f => f.FileName.ToLower() == file.FileName.ToLower()))
    {
        throw new Exception($"Já existe um arquivo com nome {file.FileName}");
    }
    
    FilesToBurn.Add(file);
    TotalFilesSize += fileSize;
    return file;
}

**3. Processo de Gravação com Notificação de Progresso**

Para implementar a notifciação de progresso durante a gravação, primeiro definimos um delegate:


/// <summary>
/// Delegate para notificação de progresso da gravação
/// </summary>
public delegate void BurnProgressChanged(BurnStatus burnStatus);

A classe BurnStatus contém informações sobre o progresso da gravação:


/// <summary>
/// Informações sobre o progresso da gravação
/// </summary>
public class BurnStatus
{
    /// <summary>
    /// Ação atual em progresso
    /// Correspondente ao enum IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION
    /// 4 - Escrevendo dados 5 - Concluído a escrita de dados 6 - Gravação concluída
    /// </summary>
    public int CurrentAction { get; set; }

    /// <summary>
    /// Nome da ação atual
    /// </summary>
    public string CurrentActionName { get; set; }

    /// <summary>
    /// Tempo decorrido em segundos
    /// </summary>
    public int ElapsedTime { get; set; }

    /// <summary>
    /// Tempo estimado total em segundos
    /// </summary>
    public int EstimatedTime { get; set; }

    /// <summary>
    /// Progresso da escrita de dados (0-1)
    /// </summary>
    public decimal Progress { get; set; }

    /// <summary>
    /// Progresso da escrita de dados em formato de porcentagem
    /// </summary>
    public string ProgressPercentage { get { return Progress.ToString("0.00%"); } }
}

Adicionamos uma propriedade delegate à classe OpticalDevice:


/// <summary>
/// Notificação de mudança de progresso da gravação
/// </summary>
public BurnProgressChanged OnProgressChanged { get; set; }

O método principal de gravação é implementado da seguinte forma:


/// <summary>
/// Inicia o processo de gravação
/// </summary>
public void Burn(string diskLabel = "Dados")
{
    if(!CanWrite)
    {
        throw new Exception("O estado atual do disco não permite gravação");
    }
    if (string.IsNullOrEmpty(diskLabel))
    {
        throw new Exception("O rótulo do disco não pode ser vazio");
    }
    if (FilesToBurn.Count <= 0)
    {
        throw new Exception("A lista de arquivos para gravar está vazia");
    }
    if(TotalFilesSize<=0)
    {
        throw new Exception("O tamanho total dos arquivos é zero");
    }

    try
    {   
        // Cria um fluxo de imagem para um diretório específico
        dynamic fileSystemImage = new IMAPI2FS.MsftFileSystemImage();  
        IMAPI2FS.IFsiDirectoryItem rootDir = fileSystemImage.Root;                
        dynamic dataWriter = new MsftDiscFormat2Data(); 

        dataWriter.Recorder = deviceController;
        dataWriter.ClientName = "GravadorCD";
        
        // Define as configurações padrão da imagem
        fileSystemImage.ChooseImageDefaults(deviceController);

        // Define informações do disco
        fileSystemImage.VolumeName = diskLabel;   
        for (int i = 0; i < FilesToBurn.Count; i++)
        {
            rootDir.AddTree(FilesToBurn[i].FilePath, true);
        }
        
        // Cria uma imagem do sistema de arquivos
        IStream stream = fileSystemImage.CreateResultImage().ImageStream;
        try
        {
            dataWriter.Update += new DDiscFormat2DataEvents_UpdateEventHandler(ProgressHandler);
            dataWriter.Write(stream); // Grava o fluxo no disco
        }
        catch (System.Exception ex)
        {
            throw ex;
        }
        finally
        {
            if (stream != null)
            {
                Marshal.FinalReleaseComObject(stream);
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Falha na gravação:{ex.Message}");
    }
}

/// <summary>
/// Manipulador de eventos para notificação de progresso
/// </summary>
/// <param name="sender"></param>Origem do evento
/// <param name="progress"></param>Informações de progresso
void ProgressHandler(dynamic sender, dynamic progress)
{
    BurnStatus status = new BurnStatus();
    try
    {
        status.ElapsedTime = progress.ElapsedTime;
        status.EstimatedTime = progress.TotalTime;
        status.CurrentAction = progress.CurrentAction;
        
        if (status.ElapsedTime > status.EstimatedTime)
        {   // Se o tempo decorrido exceder o tempo estimado, ajusta o tempo estimado
            status.EstimatedTime = status.ElapsedTime;
        }
        
        int currentAction = progress.CurrentAction;
        switch (currentAction)
        {
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_CALIBRATING_POWER:
                status.CurrentActionName = "Calibrando Potência (OPC)";
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_COMPLETED:
                status.CurrentActionName = "Gravação Concluída";
                status.Progress = 1;
                status.EstimatedTime = status.ElapsedTime;  // Ajusta tempo estimado ao tempo real
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FINALIZATION:
                status.CurrentActionName = "Finalizando a Gravação";
                status.Progress = 1;
                status.EstimatedTime = status.ElapsedTime;  // Ajusta tempo estimado ao tempo real
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_FORMATTING_MEDIA:
                status.CurrentActionName = "Formatando o Disco";
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_INITIALIZING_HARDWARE:
                status.CurrentActionName = "Inicializando Hardware";
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VALIDATING_MEDIA:
                status.CurrentActionName = "Validando o Disco";
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_VERIFYING:
                status.CurrentActionName = "Verificando os Dados";
                break;
            case (int)IMAPI2.IMAPI_FORMAT2_DATA_WRITE_ACTION.IMAPI_FORMAT2_DATA_WRITE_ACTION_WRITING_DATA:
                dynamic totalSectors;
                dynamic writtenSectors;
                dynamic startLba;
                dynamic lastWrittenLba;
                totalSectors = progress.SectorCount;
                startLba = progress.StartLba;
                lastWrittenLba = progress.LastWrittenLba;                
                writtenSectors = lastWrittenLba - startLba;                        
                status.CurrentActionName = "Escrevendo Dados";
                status.Progress = Convert.ToDecimal(writtenSectors)/ Convert.ToDecimal(totalSectors);
                break;
            default:
                status.CurrentActionName = "Ação Desconhecida";
                break;
        }
    }
    catch (Exception ex)
    {
        status.CurrentActionName = ex.Message;
    }
    
    if (OnProgressChanged != null)
    {
        OnProgressChanged(status);
    }
}

**Correção de Bug no Cálculo de Tamanho do Disco**

Durante o desenvolvimento, enfrentamos um problema onde o tamanho do disco era retornado como negativo. A causa era um overflow no tipo de dado int ao calcular o tamanho total e o espaço livre. A solução foi alterar o fator de multiplicação de 2048 para 2048L para usar um tipo de dado long e evitar o overflow.

No código original:


// Espaço disponível
m_nUseableSize = msFormat.FreeSectorsOnMedia * 2048;
// Tamanho total
m_nDiskSize = msFormat.TotalSectorsOnMedia * 2048;

Corrigido para:


// Espaço disponível
m_nUseableSize = msFormat.FreeSectorsOnMedia * 2048L;
// Tamanho total
m_nDiskSize = msFormat.TotalSectorsOnMedia * 2048L;

**Conclusão**

Este artigo demonstrou uma implementação simplificada para funcionalidades básicas de gravação de CDs em C#. Abordamos a listagem de unidades ópticas, obtenção de informações do disco, adição de arquivos para gravação, o processo de gravação com notificação de progresso, e uma correção de bug comum.

A implementação utiliza a API IMAPI2 do Windows, que é a interface padrão para operações de gravação de discos. A abordagem apresentada mantém a simplicidade enquanto fornece funcionalidades essenciais para aplicações que necessitem de recursos de gravação de CDs.

Tags: C# IMAPI2 gravação de discos .NET COM

Publicado em 6-14 00:44 por Thomas