# 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 - [x] Atualizar `Cargo.toml` com as dependências (`rfd = "0.14"` adicionado para diálogos nativos) - [x] Criar a estrutura de pastas conforme especificado - [x] Declarar os módulos em `src/main.rs` - [x] 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` | | `MkvOutput` | `path: FilePath` | | `Project` | Entidade raiz — ver abaixo | #### Estrutura de `Project` ```rust pub struct Project { pub source: VideoFile, pub tracks: Vec, // faixas externas adicionadas pelo usuário pub existing_tracks: Vec, // faixas lidas do arquivo via ffprobe pub output: MkvOutput, } ``` ### Testes obrigatórios - [x] `SyncOffset::from_seconds_str("1.2")` → `SyncOffset(1200)` - [x] `SyncOffset::from_seconds_str("-0.5")` → `SyncOffset(-500)` - [x] `Project` não aceita `output.path` igual a `source.path` - [x] `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/`) ```rust // MediaInfoPort: inspeciona faixas de um arquivo de mídia pub trait MediaInfoPort { fn probe(&self, path: &FilePath) -> Result>; } // MediaProcessorPort: executa o processamento final pub trait MediaProcessorPort { fn execute(&self, args: Vec) -> 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 - [x] Usar mocks dos ports (sem chamar nenhum processo externo) - [x] `GenerateOutput` sempre produz comando com `-c copy` (verificado via mock) - [x] `AdjustSync` com `TrackId` inexistente retorna erro - [x] `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` 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 ``` - [x] Implementar `FfmpegCommandBuilder::build(project: &Project) -> Vec` - [x] Implementar `FfmpegCommandBuilder::build_export(source, track, output) -> Vec` (exportação de faixas individuais) - [x] 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 ``` e mapeia a saída JSON para `Vec`. - [x] Implementar parsing de JSON via `serde_json` - [x] Mapear `codec_type` para `TrackKind` - [x] Mapear `tags.language` para `Option` #### `FfmpegGateway` Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura stderr. - [x] Capturar stderr para exibição de erros (RF-07) - [x] Retornar erro com mensagem legível em caso de código de saída não-zero ### Filesystem (`src/adapters/filesystem/`) - [x] `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/` - [x] Implementar execução via `tokio::process::Command` (assíncrono, RNF-05) - [x] Capturar stderr em stream via `run_ffmpeg_async` com `std::sync::mpsc::Sender` (progresso em tempo real — RF-07) - [x] Cancelamento de execução via `tokio::sync::oneshot` + `child.kill().await` ### Validação na inicialização - [x] Verificar se `ffmpeg` está disponível no `PATH` (DT-06) - [x] Verificar se `ffprobe` está disponível no `PATH` - [x] 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`) - [x] Implementar `eframe::App` para `App` - [x] `App` contém `project: Option` como única fonte de verdade do estado - [x] Despachar eventos de UI para os use cases correspondentes - [x] Integrar execução assíncrona (tokio + `std::thread`) com o loop de UI do eframe - [x] Botão "Cancelar" via `cancel_tx: Option>` - [x] `ExecutionState`: `Idle | Running | Success | Cancelled | Error` --- ## Critérios de Conclusão (v1.0) Alinhados com o PRD seção 12: - [x] É possível selecionar um arquivo de vídeo base - [x] Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via `ffprobe`) - [x] É possível adicionar uma ou mais faixas de áudio externas - [x] É possível adicionar uma ou mais faixas de legenda - [x] É possível definir offset de sincronização por faixa ao adicionar uma nova faixa - [x] É possível editar o offset de sincronização de uma faixa já existente no arquivo - [x] É possível atribuir idioma a cada faixa - [x] O arquivo MKV é gerado corretamente ao confirmar - [x] Erros do FFmpeg são exibidos de forma legível - [x] A interface não trava durante o processamento - [x] Nenhum reencoding ocorre (verificável via `ffprobe` no arquivo de saída) - [x] O flag `-c copy` está sempre presente no comando gerado (verificável via testes unitários)