9.6 KiB
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.tomlcom as dependências: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, nenhumuse 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)Projectnão aceitaoutput.pathigual asource.pathTrackIdé 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)
GenerateOutputsempre produz comando com-c copy(verificado via mock)AdjustSynccomTrackIdinexistente retorna erroAddAudioTrackemProjectsemsourceretorna 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<String> de argumentos para o FFmpeg.
Regras invariantes:
-c copysempre 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> - 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_typeparaTrackKind - Mapear
tags.languageparaOption<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 (usar craterfd)
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
ffmpegestá disponível noPATH(DT-06) - Verificar se
ffprobeestá disponível noPATH - 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::AppparaApp Appcontémproject: Projectcomo ú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
ffprobeno arquivo de saída) - O flag
-c copyestá sempre presente no comando gerado (verificável em modo debug)