Files
simple-multimidia-track-aud…/DEVELOPMENT_PLAN.md
T

9.7 KiB

Plano de Desenvolvimento — Simple Multimedia Track Audio Editor

Versão: 1.1
Data: 28/02/2026
Status: Concluído — todas as fases implementadas, 33 testes passando
Referência: PRD v1.1


Estratégia Geral

O desenvolvimento segue a Clean Architecture de dentro para fora: as camadas mais internas (domínio) são implementadas e testadas primeiro, sem qualquer dependência de I/O, frameworks ou processos externos. A UI é sempre a última camada a ser construída.

Fase 1: Setup
  └── Fase 2: Domain (testável, sem I/O)
        └── Fase 3: Application (testável com mocks)
              └── Fase 4: Adapters (integração com FFmpeg)
                    └── Fase 5: Infrastructure (processo real)
                          └── Fase 6: UI (montagem final)

Cada fase deve estar compilando e com testes passando antes de avançar para a próxima.


Fase 1 — Setup do Projeto

Objetivo: Preparar o ambiente antes de escrever qualquer lógica de negócio.

Tarefas

  • Atualizar Cargo.toml com as dependências (rfd = "0.14" adicionado para diálogos nativos)
  • Criar a estrutura de pastas conforme especificado
  • Declarar os módulos em src/main.rs
  • Verificar que o projeto compila (cargo check)

Fase 2 — Domain (núcleo puro)

Objetivo: Modelar o problema de negócio sem nenhum acoplamento a frameworks, I/O ou processos externos.

Regra: nenhum use std::process, nenhum use eframe, nenhuma chamada de rede ou filesystem nessa camada.

Value Objects (src/domain/value_objects/)

Tipo Implementação
FilePath Newtype sobre PathBuf; derivar Clone, Debug, Serialize, Deserialize
TrackId Newtype opaco sobre u32; derivar Clone, Copy, Debug, PartialEq, Eq, Hash
SyncOffset Newtype sobre i64 (milissegundos, nunca f64); implementar método from_seconds_str e as_ms
TrackLanguage Newtype sobre String (ex: "por", "eng"); validar formato ISO 639-2

Entities (src/domain/entities/)

Tipo Campos principais
VideoFile path: FilePath
AudioTrack id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage
SubtitleTrack id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage
MediaTrackInfo id: TrackId, kind: TrackKind (enum: Video/Audio/Subtitle), codec: String, language: Option<TrackLanguage>
MkvOutput path: FilePath
Project Entidade raiz — ver abaixo

Estrutura de Project

pub struct Project {
    pub source: VideoFile,
    pub tracks: Vec<Track>,              // faixas externas adicionadas pelo usuário
    pub existing_tracks: Vec<MediaTrackInfo>, // faixas lidas do arquivo via ffprobe
    pub output: MkvOutput,
}

Testes obrigatórios

  • SyncOffset::from_seconds_str("1.2")SyncOffset(1200)
  • SyncOffset::from_seconds_str("-0.5")SyncOffset(-500)
  • Project não aceita output.path igual a source.path
  • TrackId é opaco (não expõe indexação interna)

Fase 3 — Application (casos de uso e ports)

Objetivo: Definir o que o sistema faz sem saber como. Ports são traits; use cases orquestram entidades.

Ports (src/application/ports/)

// MediaInfoPort: inspeciona faixas de um arquivo de mídia
pub trait MediaInfoPort {
    fn probe(&self, path: &FilePath) -> Result<Vec<MediaTrackInfo>>;
}

// MediaProcessorPort: executa o processamento final
pub trait MediaProcessorPort {
    fn execute(&self, args: Vec<String>) -> Result<()>;
}

// FileSystemPort: abstrai acesso ao sistema de arquivos
pub trait FileSystemPort {
    fn exists(&self, path: &FilePath) -> bool;
}

Use Cases (src/application/use_cases/)

Implementar nesta ordem (dependência crescente):

# Use Case Descrição
1 LoadMediaInfo Usa MediaInfoPort para popular Project::existing_tracks
2 AddAudioTrack Adiciona AudioTrack externo ao Project::tracks
3 AddSubtitle Adiciona SubtitleTrack externo ao Project::tracks
4 AdjustSync Altera SyncOffset de uma faixa existente pelo TrackId
5 EditExistingTrackSync Ajusta offset de faixa já presente no arquivo original
6 SetTrackLanguage Altera idioma de uma faixa pelo TrackId
7 GenerateOutput Constrói o comando final e delega ao MediaProcessorPort

Testes obrigatórios

  • Usar mocks dos ports (sem chamar nenhum processo externo)
  • GenerateOutput sempre produz comando com -c copy (verificado via mock)
  • AdjustSync com TrackId inexistente retorna erro
  • AddAudioTrack retorna erro se source e output conflitarem (coberto por Project::new)

Fase 4 — Adapters (implementações concretas)

Objetivo: Conectar o domínio ao FFmpeg e ao filesystem real.

FFmpeg (src/adapters/ffmpeg/)

FfmpegCommandBuilder

Responsabilidade única: converter Project em Vec<String> de argumentos para o FFmpeg.

Regras invariantes:

  • -c copy sempre presente (RNF-01 — nunca opcional, nunca configurável)
  • SyncOffset(i64 ms)-itsoffset 1.200 (conversão feita somente aqui)
  • TrackId-map 0:a:N (mapeamento de índice feito somente aqui)

Exemplo de saída esperada:

ffmpeg -i input.mkv -itsoffset 1.200 -i audio_pt.aac -map 0:v -map 0:a -map 1:a -c copy -metadata:s:a:1 language=por output.mkv
  • Implementar FfmpegCommandBuilder::build(project: &Project) -> Vec<String>
  • Implementar FfmpegCommandBuilder::build_export(source, track, output) -> Vec<String> (exportação de faixas individuais)
  • Testes: verificar presença de -c copy, ordem dos -map, formato de -itsoffset

FfprobeGateway

Implementa MediaInfoPort. Executa:

ffprobe -v quiet -print_format json -show_streams <path>

e mapeia a saída JSON para Vec<MediaTrackInfo>.

  • Implementar parsing de JSON via serde_json
  • Mapear codec_type para TrackKind
  • Mapear tags.language para Option<TrackLanguage>

FfmpegGateway

Implementa MediaProcessorPort. Executa o processo real do FFmpeg e captura stderr.

  • Capturar stderr para exibição de erros (RF-07)
  • Retornar erro com mensagem legível em caso de código de saída não-zero

Filesystem (src/adapters/filesystem/)

  • FilePickerAdapter: abstrai seleção de arquivo via diálogo nativo (crate rfd; inclui save_audio e save_subtitle por codec)

Fase 5 — Infrastructure

Objetivo: Execução real e não-bloqueante de processos externos.

Módulo src/infrastructure/process/

  • Implementar execução via tokio::process::Command (assíncrono, RNF-05)
  • Capturar stderr em stream via run_ffmpeg_async com std::sync::mpsc::Sender<String> (progresso em tempo real — RF-07)
  • Cancelamento de execução via tokio::sync::oneshot + child.kill().await

Validação na inicialização

  • Verificar se ffmpeg está disponível no PATH (DT-06)
  • Verificar se ffprobe está disponível no PATH
  • Exibir mensagem de erro global (global_error) na UI se algum dos dois estiver ausente

Fase 6 — UI (camada mais externa)

Objetivo: Interface gráfica que conecta o usuário ao Project via use cases.

Todo estado da aplicação vive no Project. A UI apenas lê e dispara use cases.
Termos técnicos do FFmpeg nunca aparecem na interface (ver PRD seção 13).

Componentes (src/ui/components/), em ordem de construção

# Componente Descrição
1 VideoSelector Seletor de arquivo de vídeo; dispara LoadMediaInfo ao confirmar
2 ExistingTrackList Lista faixas detectadas (áudio/legenda); permite editar offset de cada uma
3 AddAudioTrackForm Formulário para adicionar faixa de áudio externa (arquivo, idioma, offset)
4 AddSubtitleForm Formulário para adicionar legenda externa (arquivo, idioma, offset)
5 SyncOffsetField Campo de offset em segundos (ex: -1.2s); converte para SyncOffset(ms) internamente
6 LanguageField Seletor/input de idioma (ex: por, eng)
7 OutputSelector Campo de caminho de saída + extensão .mkv forçada
8 ExecutionPanel Botão "Gerar MKV", exibição de progresso e erros legíveis

App (src/ui/app.rs)

  • Implementar eframe::App para App
  • App contém project: Option<Project> como única fonte de verdade do estado
  • Despachar eventos de UI para os use cases correspondentes
  • Integrar execução assíncrona (tokio + std::thread) com o loop de UI do eframe
  • Botão "Cancelar" via cancel_tx: Option<oneshot::Sender<()>>
  • ExecutionState: Idle | Running | Success | Cancelled | Error

Critérios de Conclusão (v1.0)

Alinhados com o PRD seção 12:

  • É possível selecionar um arquivo de vídeo base
  • Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via ffprobe)
  • É possível adicionar uma ou mais faixas de áudio externas
  • É possível adicionar uma ou mais faixas de legenda
  • É possível definir offset de sincronização por faixa ao adicionar uma nova faixa
  • É possível editar o offset de sincronização de uma faixa já existente no arquivo
  • É possível atribuir idioma a cada faixa
  • O arquivo MKV é gerado corretamente ao confirmar
  • Erros do FFmpeg são exibidos de forma legível
  • A interface não trava durante o processamento
  • Nenhum reencoding ocorre (verificável via ffprobe no arquivo de saída)
  • O flag -c copy está sempre presente no comando gerado (verificável via testes unitários)