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

407 lines
20 KiB
Markdown

# 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<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
```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<Track> // faixas externas adicionadas
> ├── existing_tracks: Vec<MediaTrackInfo> // 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<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` |
| 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<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.**
#### 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.