# Plano de Desenvolvimento — Simple Multimedia Track Audio Editor **Versão:** 1.0 **Data:** 28/02/2026 **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: ```toml eframe = "0.27" anyhow = "1" serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["process", "rt-multi-thread", "macros"] } serde_json = "1" ``` - [ ] Criar a estrutura de pastas: ``` src/ ├── domain/ │ ├── entities/ │ └── value_objects/ ├── application/ │ ├── use_cases/ │ └── ports/ ├── adapters/ │ ├── ffmpeg/ │ └── filesystem/ ├── infrastructure/ │ └── process/ └── ui/ ├── components/ └── app.rs ``` - [ ] 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` | | `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 - [ ] `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/`) ```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 - [ ] 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` em `Project` sem `source` retorna erro --- ## 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 ``` - [ ] Implementar `FfmpegCommandBuilder::build(project: &Project) -> Vec` - [ ] 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`. - [ ] Implementar parsing de JSON via `serde_json` - [ ] Mapear `codec_type` para `TrackKind` - [ ] Mapear `tags.language` para `Option` #### `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 (usar crate `rfd`) --- ## 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 stdout e stderr em stream (para exibir progresso em tempo real — RF-07) ### 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 clara ao usuário 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: Project` como única fonte de verdade do estado - [ ] Despachar eventos de UI para os use cases correspondentes - [ ] Integrar execução assíncrona (tokio) com o loop de UI do eframe --- ## 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 em modo debug)