# PRD — Simple Multimedia Track Audio Editor **Versão:** 1.2 **Data:** 28/02/2026 **Status:** Em desenvolvimento **Revisão:** v1.2 — Adicionado RF-09 (Modo Lote); batch promovido de backlog para requisito planejado --- ## 1. Visão do Produto Um editor simples de faixas multimídia que permite ao usuário combinar vídeo, áudio e legendas em um único arquivo de saída no formato MKV, sem realizar reencoding — apenas mux e ajuste de timestamps. --- ## 2. Problema Usuários domésticos e de automação precisam de uma ferramenta simples para: - Adicionar faixas de áudio alternativas a um vídeo (ex: dublagem, comentários) - Adicionar legendas - Ajustar a sincronização entre faixas - Gerar um arquivo final organizado e compatível As ferramentas existentes são complexas (ex: interface direta do FFmpeg via CLI) ou pesadas demais para esse caso de uso. --- ## 3. Objetivos - Fornecer uma interface gráfica simples e intuitiva - Abstrair a complexidade do FFmpeg para o usuário final - Gerar arquivos MKV com múltiplas faixas de áudio e legendas - Permitir ajuste de sincronização por faixa - Não realizar reencoding (apenas mux) - Ser multiplataforma (Linux, Windows, macOS) --- ## 4. Fora do Escopo (v1.0) - Reencoding / transcodificação de vídeo ou áudio - Preview de vídeo embutido - Detecção automática de sincronização - Remoção de faixas existentes do arquivo original - Seleção de faixa padrão no container - Edição de corte ou splice de vídeo --- ## 5. Usuários-Alvo - Usuário doméstico que deseja adicionar dublagem ou áudio alternativo a vídeos - Operadores de automação de mídia que precisam empacotar faixas em lote - Usuários técnicos confortáveis com instalação de dependências (FFmpeg) --- ## 6. Requisitos Funcionais ### RF-01 — Adicionar faixa de áudio - O usuário pode selecionar um arquivo de áudio externo - O áudio é mapeado ao container de saída via `ffmpeg -map` - Suporta múltiplas faixas de áudio ### RF-02 — Adicionar legenda - O usuário pode selecionar um arquivo de legenda (ex: `.srt`, `.ass`) - A legenda é mapeada ao container via `ffmpeg -map` - Suporta múltiplas faixas de legenda ### RF-03 — Ajustar sincronização - O usuário pode definir um offset por faixa, expresso em **segundos com uma casa decimal** na interface (ex: `1.2s`, `-0.5s`) - Internamente armazenado como `SyncOffset(i64)` em milissegundos — sem ponto flutuante - Implementado via `ffmpeg -itsoffset`; a conversão ms→string é feita exclusivamente no `FfmpegCommandBuilder` - Aceita valores positivos e negativos ### RF-04 — Definir idioma da faixa - O usuário pode atribuir um código de idioma a cada faixa (ex: `por`, `eng`) - Implementado via `ffmpeg -metadata:s` ### RF-10 — Definir nome da faixa - O usuário pode preencher um título livre para cada faixa externa adicionada (ex: `Português`, `Comentários`) - O campo aceita string vazia, que instrui o `FfmpegCommandBuilder` a emitir `title=` limpando qualquer título herdado do arquivo fonte - Objetivo principal: sobrescrever títulos gerados automaticamente por ferramentas externas (ex: `"ISO Media file produced by Google Inc."` em arquivos M4A do Google) que confundem players como Jellyfin mobile - Implementado via `ffmpeg -metadata:s:a:{idx} title=` ou `-metadata:s:s:{idx} title=`; sempre emitido, mesmo quando vazio ### RF-11 — Definir faixa padrão - O usuário pode marcar uma faixa externa como “faixa padrão” do container MKV - Quando marcada, o `FfmpegCommandBuilder` emite `-disposition:a/s:{idx} default` para a faixa selecionada e `-disposition:a/s:{orig_idx} 0` para todas as faixas existentes do mesmo tipo, removendo o flag `default=1` que o arquivo fonte carrega - Objetivo: garantir que players como Jellyfin selecionem automaticamente a faixa preferida sem depender de configuração manual no side do player - Implementado via `ffmpeg -disposition:a:{idx} default` ### RF-05 — Selecionar arquivo de vídeo base - O usuário seleciona o arquivo de vídeo de entrada - O stream de vídeo é preservado sem modificação ### RF-06 — Gerar arquivo de saída - O usuário define o caminho e nome do arquivo de saída - O container de saída é sempre MKV - A geração é executada via `ffmpeg` em processo externo ### RF-07 — Feedback de execução - A interface exibe o progresso e resultado da operação - Erros do FFmpeg (via stderr) são exibidos ao usuário de forma legível ### RF-08 — Editar faixas existentes de áudio e legenda - O usuário pode selecionar e editar faixas de áudio ou legenda já presentes no arquivo de vídeo de entrada - É possível ajustar o offset de sincronização de uma faixa existente, sem necessidade de adicionar uma nova faixa - A faixa original é remapeada com o novo timestamp via `ffmpeg -itsoffset` combinado com `-map` - Útil para corrigir atrasos em faixas de dublagem ou legenda já incorporadas ao arquivo ### RF-09 — Processamento em lote - A interface oferece duas abas: **Projeto Único** (fluxo atual) e **Lote** - A aba Lote funciona como um **carrinho**: o usuário adiciona, revisa e remove itens livremente antes de processar - Cada item é um projeto completo e independente (vídeo, faixas e saída próprios), configurado via **formulário inline** na própria aba Lote — sem navegação para outra tela - A seleção de arquivos usa **diálogos nativos** (igual ao modo Projeto Único) — nenhum path é digitado manualmente - O processamento é **sequencial** — um projeto por vez, sem paralelismo, para evitar sobrecarga de I/O - Durante o processamento, o carrinho é bloqueado: não é possível adicionar nem remover itens - Cada item exibe seu estado individual: `Aguardando | Processando | Concluído | Erro` - É possível cancelar o item em execução; os demais permanecem no carrinho com estado `Aguardando` --- ## 7. Requisitos Não Funcionais ### RNF-01 — Sem reencoding (regra central do domínio) Nenhum stream de vídeo ou áudio deve ser recodificado. Apenas mux e ajuste de timestamps são permitidos. **Esta é a propriedade mais importante do produto.** O flag `-c copy` (ou equivalente por stream) **nunca é opcional**. Deve ser emitido pelo `FfmpegCommandBuilder` de forma incondicional, independente de qualquer configuração do usuário. O domínio deve expor uma invariante que o garanta: ``` impl FfmpegCommandBuilder { // -c copy é sempre adicionado. Não existe API para desativá-lo. fn build(&self, project: &Project) -> Vec { ... } } ``` Qualquer violação dessa regra invalida a proposta de valor do produto. ### RNF-02 — Desempenho A operação de mux deve ser concluída em tempo proporcional ao tamanho do arquivo, sem bloqueio da interface. ### RNF-03 — Compatibilidade O arquivo de saída deve ser compatível com players modernos que suportam MKV (ex: VLC, mpv, Jellyfin). ### RNF-04 — Dependência externa FFmpeg deve estar instalado no sistema. A aplicação não o embute por padrão, porém pode ser distribuída junto. ### RNF-05 — Interface responsiva A interface não deve travar durante a execução do FFmpeg. A execução deve ser assíncrona. --- ## 8. Arquitetura Técnica ``` Interface (egui/eframe) ↓ Backend Rust (validação, construção de comandos) ↓ Processo externo (FFmpeg via std::process::Command) ↓ Arquivo de saída (.mkv) ``` ### Stack | Camada | Tecnologia | Motivo | | ------------ | ------------------------------ | ------------------------------------ | | Interface | `egui` / `eframe` | Nativo, simples, multiplataforma | | Backend | Rust (`std::process::Command`) | Estável, sem dependências externas | | Erros | `anyhow` | Propagação de erros simplificada | | Configuração | `serde` | Serialização de perfis e histórico | | Async | `tokio` (opcional) | Execução não bloqueante da interface | | Mídia | FFmpeg (externo) | Maduro, estável, amplamente testado | | Container | MKV | Melhor suporte a múltiplas faixas | ### Dependências Cargo ```toml eframe = "0.27" anyhow = "1" serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["process"] } # opcional ``` --- ## 9. Arquitetura de Projeto — Clean Architecture A organização do código segue os princípios da **Clean Architecture**, garantindo separação de responsabilidades, testabilidade e baixo acoplamento entre camadas. ### Regra de dependência As dependências sempre apontam de fora para dentro. Camadas internas não conhecem camadas externas. ``` ┌──────────────────────────────────────────┐ │ Infrastructure / UI │ ← egui, FFmpeg, filesystem │ ┌────────────────────────────────────┐ │ │ │ Adapters │ │ ← FfmpegGateway, FilePicker │ │ ┌──────────────────────────────┐ │ │ │ │ │ Application │ │ │ ← Use Cases │ │ │ ┌────────────────────────┐ │ │ │ │ │ │ │ Domain │ │ │ │ ← Entities, Value Objects │ │ │ └────────────────────────┘ │ │ │ │ │ └──────────────────────────────┘ │ │ │ └────────────────────────────────────┘ │ └──────────────────────────────────────────┘ ``` ### Camadas #### Domain (núcleo) Contém as entidades e objetos de valor do negócio. Não depende de nada externo. | Tipo | Exemplos | | ------------- | ------------------------------------------------------------------------------------ | | Entities | `Project`, `VideoFile`, `AudioTrack`, `SubtitleTrack`, `MkvOutput`, `MediaTrackInfo` | | Value Objects | `TrackId`, `SyncOffset`, `TrackLanguage`, `FilePath` | > **`Project`** é a entidade central do domínio. Representa a sessão de edição completa do usuário e deve ser a única fonte de verdade do estado em memória. > > ``` > Project > ├── source: VideoFile > ├── tracks: Vec // faixas externas adicionadas > ├── existing_tracks: Vec // faixas lidas do arquivo via ffprobe > └── output: MkvOutput > ``` > > **`TrackId`** abstrai os índices de stream do FFmpeg. Internamente é um identificador opaco (`u32` ou `String`). Nenhuma lógica de negócio deve depender de índices FFmpeg diretamente — isso é responsabilidade do adapter. > > **`SyncOffset`** armazena o offset de sincronização em **milissegundos como inteiro** (`i64`). Nunca como `f64`. O uso de ponto flutuante acumula erro de precisão; o adapter é responsável por converter para o formato exigido pelo FFmpeg (`-itsoffset`). #### Application (casos de uso) Contém as regras de negócio da aplicação. Orquestra as entidades do domínio. Define traits (ports) que as camadas externas devem implementar. | Tipo | Exemplos | | --------- | ---------------------------------------------------------------------------------------------------------------------------- | | Use Cases | `LoadMediaInfo`, `AddAudioTrack`, `AddSubtitle`, `AdjustSync`, `EditExistingTrackSync`, `SetTrackLanguage`, `GenerateOutput` | | Ports | `MediaProcessorPort`, `MediaInfoPort`, `FileSystemPort` | > **`MediaInfoPort`** é o port responsável por inspecionar arquivos de mídia existentes. Deve ser definido na camada Application e implementado na camada Adapters via `FfprobeGateway`. > > ``` > trait MediaInfoPort { > fn probe(path: &FilePath) -> Result>; > } > ``` > > `MediaTrackInfo` contém: `TrackId`, tipo de stream (vídeo/áudio/legenda), codec, idioma detectado. > > **`LoadMediaInfo`** é o caso de uso disparado quando o usuário seleciona um arquivo de vídeo. Lê as faixas existentes e popula o `Project::existing_tracks`. #### Adapters Implementam os ports definidos na camada de Application. Traduzem dados entre o domínio e o mundo externo. | Tipo | Exemplos | | ------------ | --------------------------------------------------------- | | Gateway | `FfmpegCommandBuilder`, `FfmpegGateway`, `FfprobeGateway` | | Presenter | `ErrorPresenter` (formata stderr do FFmpeg) | | File Adapter | `FilePickerAdapter` | > **`FfprobeGateway`** implementa `MediaInfoPort`. Executa `ffprobe -v quiet -print_format json -show_streams` e mapeia a saída para `Vec`. Não conhece o domínio além das structs que está populando. > > **`FfmpegCommandBuilder`** é responsável por converter `TrackId` para índices `-map 0:a:N` do FFmpeg e `SyncOffset` (ms inteiro) para o formato `-itsoffset 1.200` aceito pelo binário. **Toda conversão de tipos internos para argumentos FFmpeg fica aqui e somente aqui.** #### Infrastructure / UI Camada mais externa. Contém o framework de UI e a execução real de processos. | Tipo | Exemplos | | ---------- | ------------------------------------------------------ | | UI | `App` (eframe), componentes egui | | Process | Execução de `std::process::Command` / `tokio::process` | | Filesystem | Leitura e escrita de arquivos | --- ### Estrutura de pastas ``` src/ ├── domain/ │ ├── entities/ # Project, VideoFile, AudioTrack, SubtitleTrack, MkvOutput, MediaTrackInfo │ └── value_objects/ # TrackId, SyncOffset (ms/i64), TrackLanguage, FilePath ├── application/ │ ├── use_cases/ # LoadMediaInfo, AddAudioTrack, AddSubtitle, AdjustSync, GenerateOutput │ └── ports/ # Traits: MediaProcessorPort, MediaInfoPort, FileSystemPort ├── adapters/ │ ├── ffmpeg/ # FfmpegCommandBuilder, FfmpegGateway, FfprobeGateway │ └── filesystem/ # FilePickerAdapter ├── infrastructure/ │ └── process/ # Execução real do FFmpeg / ffprobe ├── ui/ │ ├── app.rs # eframe App (ponto de entrada da interface) │ └── components/ # Componentes egui reutilizáveis └── main.rs ``` --- ## 10. Débitos Técnicos | ID | Descrição | Impacto | Mitigação | | ----- | ----------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------ | | DT-01 | FFmpeg não embutido | Usuário precisa instalar | Distribuir junto com a aplicação | | DT-02 | Dependência de processo externo | Menor controle interno | Encapsular via módulo de serviço | | DT-03 | Parsing de erros do FFmpeg (stderr) | Necessário tratamento manual | Parsear saída e exibir mensagem amigável | | DT-04 | Compatibilidade de codecs | Alguns codecs podem não ser aceitos | Usar MKV como container padrão | | DT-05 | Performance de processo externo | Pequeno overhead | Aceitável — mux é rápido | | DT-06 | ffprobe como dependência adicional | Parsing de JSON da saída do ffprobe | Validar presença de ffprobe na inicialização; exibir mensagem clara se ausente | | DT-07 | Compatibilidade com Jellyfin mobile | Faixas não selecionadas automaticamente | Investigando; as RFC RF-10 e RF-11 mitigam parcialmente — `title` e `disposition` são emitidos corretamente, porém o Jellyfin mobile pode ter comportamento diferente do web; requer testes com arquivo gerado | --- ## 11. Funcionalidades Futuras (Backlog) - Preview de vídeo embutido na interface - Detecção automática de sincronização entre faixas - Seleção de faixa padrão no container MKV - Remoção de faixas existentes do arquivo original - Suporte a perfis de configuração salvos --- ## 12. Critérios de Aceitação (v1.0) - [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 de áudio ou legenda 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`) - [x] O flag `-c copy` está sempre presente no comando gerado (verificável via testes unitários e modo debug) --- ## 13. Princípios de UX > Este software é um **tradutor** entre a confusão do FFmpeg e a ordem que o usuário precisa. O FFmpeg é uma ferramenta de poder absurdo — e opacidade equivalente. O usuário não entende `track index`, `stream mapping` ou `itsoffset`. O usuário entende: - _"Áudio em português está 1.2 segundos atrasado"_ - _"Quero adicionar a legenda em inglês"_ - _"Gerar o arquivo final"_ Toda decisão de UX deve partir dessa perspectiva. Termos técnicos do FFmpeg nunca devem aparecer na interface. | FFmpeg (interno) | Interface (usuário) | | -------------------- | ------------------------------ | | `-itsoffset -1200ms` | "Adiantar 1.2s" | | `stream 0:a:1` | "Faixa de áudio 2 — Português" | | `-c copy` | _(invisível — nunca exposto)_ | | `ffprobe output` | Lista de faixas detectadas | --- ## 14. Insight Arquitetural — MKV como Banco de Dados > Você não está editando vídeo. Você está **editando metadados de um container**. O MKV (Matroska) é estruturalmente um banco de dados multimídia. Suas operações são: - **INSERT** uma faixa de áudio ou legenda - **UPDATE** o timestamp (offset) de uma faixa existente - **SELECT** as faixas para inspecionar via ffprobe O arquivo de saída não é uma criação nova — é uma nova **visão** do container original com faixas adicionadas e metadados ajustados, sem tocar nos bytes de mídia. Esse modelo mental simplifica a implementação: - Sem buffers de vídeo - Sem pipelines de transcoding - Apenas mapeamento de streams e metadados Quando a equipe pensa assim, o código fica limpo, as abstrações fazem sentido e os bugs diminuem.