27 KiB
PRD — Simple Multimedia Track Audio Editor
Versão: 1.4
Data: 01/03/2026
Status: v1.0 funcional — Fases 1–9 concluídas; 33 testes passando; aplicação executável
Revisão: v1.4 — Adicionado RF-15 (Excluir faixa existente do output)
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
- Edição de corte ou splice de vídeo
- Salvamento automático em disco (sem intervenção do usuário)
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 | ⊸ Cancelado | ✗ Erro - É possível cancelar o item em execução; os demais permanecem no carrinho com estado
Aguardando - O dispatch FFmpeg/mkvmerge por item é automático: se qualquer faixa do item tiver
drift_scale ≠ 1.0, o motor mkvmerge é usado
RF-12 — Correção de drift progressivo de sincronização
- O usuário pode especificar um fator de velocidade original da faixa em porcentagem (ex:
99.983%) - Internamente representado como
drift_scale: f64em cada faixa (AudioTrack,SubtitleTrack,MediaTrackInfo) - Implementado via
mkvmerge --sync TID:DELAY,NUM/DEN— aplica escala de timestamps no nível do container, sem reencoding - Dispatch automático: se qualquer faixa tiver
scale ≠ 1.0, o projeto inteiro usa o pipelinemkvmergeem vez do FFmpeg - Campo exibido na interface como "Velocidade original (%)" — jamais expõe termos técnicos como
scale,driftoumkvmerge - O campo é desabilitado com tooltip explicativo quando
mkvmergenão está disponível noPATH - Funciona tanto no modo Projeto Único quanto no modo Lote
- Preserva a invariante RNF-01: nenhum byte de mídia é reprocessado
RF-13 — Exportar faixa existente
- A partir da lista de faixas detectadas pelo
ffprobe, o usuário pode exportar qualquer faixa de áudio ou legenda para um arquivo separado - O formato do arquivo de saída é inferido a partir do codec da faixa (ex: AAC →
.aac, SRT →.srt) - Implementado via
FfmpegCommandBuilder::build_export()com-c:a copypara áudio; legendas omitem-c(conversão de container de texto, sem processamento de mídia) - O diálogo de salvamento usa filtro por extensão correspondente ao codec detectado
RF-14 — Persistência de sessão
- O usuário pode salvar o estado completo do projeto em disco com o botão "💾 Salvar sessão"
- O estado é restaurado com o botão "📂 Carregar sessão"
- Formato: JSON via
serde_json; local:~/.config/simple-mkv-editor/session.json(Linux) /AppData(Windows) - Feedback visual no header: indicador verde (sucesso) ou vermelho (erro) com botão ✕ para dispensar
- Erros de desserialização (arquivo corrompido ou versão incompatível) são tratados com mensagem amigável
- Salvamento automático não é realizado — apenas sob demanda do usuário
RF-15 — Excluir faixa existente do arquivo de saída
- O usuário pode marcar qualquer faixa de áudio ou legenda já presente no arquivo de entrada para ser omitida do arquivo de saída
- A operação é reversível: a faixa pode ser restaurada antes da geração do MKV
- Faixas marcadas como excluídas são exibidas com texto tachado e cor esmaecida na lista; seus campos de offset e drift ficam desabilitados
- Faixas de vídeo e dados não podem ser excluídas — somente áudio e legenda
- Implementado via campo
excluded: boolemMediaTrackInfo; métodotoggle_existing_track_excluded()emProject - Ambos os pipelines (FFmpeg e mkvmerge) respeitam o flag: faixas excluídas são filtradas antes da geração dos argumentos de
-map/--audio-tracks/--subtitle-tracks - O estado
excludedé persistido junto com a sessão (RF-14)
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ências externas
FFmpeg e ffprobe devem estar instalados no sistema. A aplicação verifica a presença de ambos na inicialização e exibe erro global se ausentes.
mkvmerge (MKVToolNix) é uma dependência opcional. Quando ausente, a funcionalidade de correção de drift (RF-12) fica desabilitada visualmente; o restante do produto continua funcional. A verificação é soft — não bloqueia a inicialização.
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 + serde_json |
Serialização de sessão em JSON |
| Async | tokio |
Execução não bloqueante da interface |
| Diálogos | rfd |
Diálogos nativos de arquivo multiplataforma |
| Mídia (mux) | FFmpeg (externo) | Mux sem reencoding; caminho padrão |
| Mídia (drift) | mkvmerge / MKVToolNix (externo) | Correção de drift sem reencoding (opcional) |
| Container | MKV | Melhor suporte a múltiplas faixas |
Dependências Cargo
eframe = "0.27"
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["process", "rt-multi-thread", "macros", "io-util", "sync"] }
rfd = "0.14"
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, SyncTransform, 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: MkvOutputMétodo relevante:
needs_mkvmerge() -> bool— retornatruese qualquer faixa tiverdrift_scale ≠ 1.0, sinalizando que o projeto deve usar o pipelinemkvmergeem vez do FFmpeg.
TrackIdabstrai os índices de stream do FFmpeg. Internamente é um identificador opaco (u32). 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).
SyncTransformé o value object que combina deslocamento e escala temporal:{ offset_ms: i64, scale: f64 }. Usado internamente para representar a transformação completa de uma faixa no pipeline mkvmerge (RF-12).scale = 1.0representa identidade;scale ≠ 1.0ativa o correto motor de dispatch.
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, AdjustDrift, AdjustExistingTrackDrift, EditExistingTrackSync, ExportTrack, RemoveTrack, SetTrackLanguage, GenerateOutput |
| Ports | MediaProcessorPort, MediaInfoPort, FileSystemPort, ContainerMuxPort |
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 |
| Gateway | MkvmergeCommandBuilder, MkvmergeGateway (motor de drift — RF-12) |
| Presenter | ErrorPresenter (formata stderr do FFmpeg / stdout do mkvmerge) |
| File Adapter | FilePickerAdapter (inclui save_audio(codec) e save_subtitle(codec)) |
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.Além do
-c copy, sempre emite-max_interleave_delta 0(evita descarte silencioso de pacotes em streams com grande delta de timestamps) e-avoid_negative_ts make_zero(normaliza timestamps negativos comuns em M4A/AAC de serviços de streaming).
MkvmergeCommandBuilderconverteProjectem argumentos para omkvmerge. Usa--sync TID:DELAY,NUM/DENpara aplicar deslocamento e escala de timestamps sem reencoding. A escalaf64é convertida para fração racionl(NUM, DEN)com precisão de 6 casas decimais.
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), SyncTransform (offset+scale), TrackLanguage, FilePath
├── application/
│ ├── use_cases/ # LoadMediaInfo, AddAudioTrack, AddSubtitle, AdjustSync, AdjustDrift,
│ │ # AdjustExistingTrackDrift, EditExistingTrackSync, ExportTrack,
│ │ # RemoveTrack, SetTrackLanguage, GenerateOutput
│ └── ports/ # Traits: MediaProcessorPort, MediaInfoPort, FileSystemPort, ContainerMuxPort
├── adapters/
│ ├── ffmpeg/ # FfmpegCommandBuilder, FfmpegGateway, FfprobeGateway
│ ├── mkvmerge/ # MkvmergeCommandBuilder, MkvmergeGateway (RF-12 — drift)
│ └── filesystem/ # FilePickerAdapter
├── infrastructure/
│ ├── process/ # run_ffmpeg_async, run_mkvmerge_async, validate_dependencies, mkvmerge_available
│ └── persistence/ # save_session, load_session (RF-14)
├── ui/
│ ├── app.rs # eframe App; ActiveTab (Single/Batch); dispatch FFmpeg/mkvmerge
│ └── components/ # VideoSelector, ExistingTrackList, AddAudioTrackForm, AddSubtitleForm,
│ # SyncOffsetField (+ campo drift), OutputSelector, ExecutionPanel, BatchPanel
└── main.rs
10. Débitos Técnicos
| ID | Descrição | Impacto | Mitigação / Status |
|---|---|---|---|
| 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 | Encapsulado via infrastructure/process; validate_dependencies() na inicialização |
| DT-03 | Parsing de erros do FFmpeg (stderr) | Mensagens brutas exibidas ao usuário | Stderr capturado linha a linha via run_ffmpeg_async; exibido no ExecutionPanel; parse amigável futuro |
| DT-04 | Compatibilidade de codecs | Alguns codecs podem não ser aceitos | MKV como container padrão; -c copy garante que não há transcodificação |
| DT-05 | Performance de processo externo | Pequeno overhead | Aceitável — mux é rápido; sem impacto perceptível |
| DT-06 | ffprobe como dependência adicional | Parsing de JSON da saída do ffprobe | ✅ Resolvido — validate_dependencies() verifica ffprobe na inicialização; erro global exibido se ausente |
| DT-07 | Compatibilidade com Jellyfin mobile | Faixas não selecionadas automaticamente | Investigando — RF-10 (title) e RF-11 (disposition) implementados; comportamento no Jellyfin mobile requer testes com arquivo gerado |
| DT-08 | mkvmerge não embutido | Usuário precisa instalar MKVToolNix | Dependência opcional; feat. de drift desabilitada visualmente se ausente; verificação soft na inicialização |
| DT-09 | Testes de integração com FFmpeg real | Regressões em comandos gerados | Pendente — requer ffmpeg no CI; coberto indiretamente pelos testes unitários do FfmpegCommandBuilder |
| DT-10 | Comando FFmpeg não visível ao usuário | Difícil de debugar manualmente | Planejado — painel colapsável com o comando gerado na ExecutionPanel (modo debug) |
11. Funcionalidades Futuras (Backlog)
- Preview de vídeo embutido na interface
- Detecção automática de sincronização entre faixas
- Suporte a perfis de configuração reutilizáveis (presets de idioma, offset, drift)
- Painel colapsável com o comando FFmpeg/mkvmerge gerado (modo debug, DT-10)
- Mensagens de erro mais amigáveis a partir do stderr do FFmpeg
- Testes de integração com FFmpeg real no CI (DT-09)
- Testes de snapshot para o
FfmpegCommandBuildereMkvmergeCommandBuildercom projetos complexos
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
- É possível definir um título/nome para cada faixa externa (RF-10)
- É possível marcar uma faixa externa como faixa padrão do container (RF-11)
- É possível exportar uma faixa existente para arquivo separado (RF-13)
- É possível salvar e restaurar a sessão em disco (RF-14)
- É possível excluir faixas de áudio ou legenda existentes do arquivo de saída, com possibilidade de restauração antes da geração (RF-15)
- O modo lote permite configurar e processar múltiplos projetos sequencialmente (RF-09)
- É possível definir fator de correção de drift por faixa (RF-12; requer mkvmerge)
- O arquivo MKV é gerado corretamente ao confirmar
- Erros do FFmpeg/mkvmerge são exibidos de forma legível na
ExecutionPanel - A interface não trava durante o processamento (execução assíncrona via
tokio) - É possível cancelar a geração em andamento
- Nenhum reencoding ocorre (verificável via
ffprobeno arquivo de saída) - O flag
-c copyestá sempre presente no comando FFmpeg gerado (verificável via testes unitários)
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.