20 KiB
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 noFfmpegCommandBuilder - 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
FfmpegCommandBuildera emitirtitle=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
FfmpegCommandBuilderemite-disposition:a/s:{idx} defaultpara a faixa selecionada e-disposition:a/s:{orig_idx} 0para todas as faixas existentes do mesmo tipo, removendo o flagdefault=1que 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
ffmpegem 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 -itsoffsetcombinado 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<String> { ... }
}
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
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<Track> // faixas externas adicionadas ├── existing_tracks: Vec<MediaTrackInfo> // faixas lidas do arquivo via ffprobe └── output: MkvOutput
TrackIdabstrai os índices de stream do FFmpeg. Internamente é um identificador opaco (u32ouString). Nenhuma lógica de negócio deve depender de índices FFmpeg diretamente — isso é responsabilidade do adapter.
SyncOffsetarmazena o offset de sincronização em milissegundos como inteiro (i64). Nunca comof64. 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 viaFfprobeGateway.trait MediaInfoPort { fn probe(path: &FilePath) -> Result<Vec<MediaTrackInfo>>; }
MediaTrackInfoconté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 oProject::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 |
FfprobeGatewayimplementaMediaInfoPort. Executaffprobe -v quiet -print_format json -show_streamse mapeia a saída paraVec<MediaTrackInfo>. Não conhece o domínio além das structs que está populando.
FfmpegCommandBuilderé responsável por converterTrackIdpara índices-map 0:a:Ndo FFmpeg eSyncOffset(ms inteiro) para o formato-itsoffset 1.200aceito 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)
- É 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 de áudio ou legenda 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) - O flag
-c copyestá 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.