Files
simple-multimidia-track-aud…/PRD.md
T

27 KiB
Raw Blame History

PRD — Simple Multimedia Track Audio Editor

Versão: 1.4
Data: 01/03/2026
Status: v1.0 funcional — Fases 19 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 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 | ⊸ 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: f64 em 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 pipeline mkvmerge em vez do FFmpeg
  • Campo exibido na interface como "Velocidade original (%)" — jamais expõe termos técnicos como scale, drift ou mkvmerge
  • O campo é desabilitado com tooltip explicativo quando mkvmerge não está disponível no PATH
  • 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 copy para á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: bool em MediaTrackInfo; método toggle_existing_track_excluded() em Project
  • 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: MkvOutput

Método relevante: needs_mkvmerge() -> bool — retorna true se qualquer faixa tiver drift_scale ≠ 1.0, sinalizando que o projeto deve usar o pipeline mkvmerge em vez do FFmpeg.

TrackId abstrai 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.

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).

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.0 representa identidade; scale ≠ 1.0 ativa 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 via FfprobeGateway.

trait MediaInfoPort {
    fn probe(path: &FilePath) -> Result<Vec<MediaTrackInfo>>;
}

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
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))

FfprobeGateway implementa MediaInfoPort. Executa ffprobe -v quiet -print_format json -show_streams e mapeia a saída para Vec<MediaTrackInfo>. 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.

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).

MkvmergeCommandBuilder converte Project em argumentos para o mkvmerge. Usa --sync TID:DELAY,NUM/DEN para aplicar deslocamento e escala de timestamps sem reencoding. A escala f64 é 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 Resolvido — painel colapsável "🛠 Comando gerado" na ExecutionPanel e em cada item do modo Lote; botão de cópia para clipboard

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)
  • 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 FfmpegCommandBuilder e MkvmergeCommandBuilder com 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 ffprobe no arquivo de saída)
  • O flag -c copy está sempre presente no comando FFmpeg gerado (verificável via testes unitários)
  • O comando FFmpeg/mkvmerge gerado é exibido em painel colapsável na ExecutionPanel (DT-10)

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.