Media — Video record, playback, screen-capture, formats (privacy + widgets)
Contrato cross-surface para captura, playback e screen-capture de vídeo em apps Koder. Toggles de privacidade (`media.video.camera`, `media.video.screen`, `media.video.mic`) em Settings; defaults seguros (tudo OFF até prompt do SO); codecs canônicos (H.264/HEVC/AV1/VP9 in via kodec; H.264 + AAC out); widgets `KoderVideoPlayer` + `KoderVideoRecorder` + `KoderScreenCapture` em `engines/sdk/koder_kit`. Recording indicator obrigatório quando capture ativo. Upload size cap 500 MB. `<APP>-VIDEO-*` error map.
Normative cross-surface spec para video I/O em apps Koder. Implementação obrigatória via widgets
KoderVideo*emengines/sdk/koder_kit(Flutter) ekoder_web_kit(JS) — nunca rolarvideo_playerupstream ou<video>raw local. Codec layer viaengines/kodec(canônica) ou Koda port (shadow-active).
Scope
Aplica-se a todo app Koder que grave, reproduza, ou faça screen-capture de vídeo: call recording, tutorial in-app, attachment de vídeo em chat, demo em landing, screen-share em ferramenta de dev, upload de vídeo de produto.
Surfaces cobertas: Flutter mobile (Android + iOS), Flutter desktop (Linux + macOS + Windows), Flutter web ou templ+HTMX, TV (playback; gravação fora de escopo em TV). CLI/TUI exigem path arg + delegam ao desktop player externo.
1 — MUST: expose "Vídeo" toggle group in Settings
Sub-seção "Vídeo" do agrupamento "Mídia" em Settings; deve conter, na ordem:
- Câmera (
media.video.camera) — gate à camera - Microfone para vídeo (
media.video.mic) — gate ao mic durante gravação - Compartilhar tela (
media.video.screen) — gate ao screen-capture - Resolução máxima (
media.video.max_height) — dropdown 480 / 720 / 1080 / 2160 - Qualidade (
media.video.bitrate_mbps) — slider 1-20 Mbps
Compartilha media.image.camera quando ambos coexistem (camera permission é granted ao app, não ao sub-uso); cada toggle controla uso, não permissão de SO.
2 — MUST: defaults seguros em fresh install
| Chave | Default | Notas |
|---|---|---|
media.video.camera |
OFF | Privacy-by-default |
media.video.mic |
OFF | Vídeo silencioso até user ativar |
media.video.screen |
OFF | Screen-capture nunca silent-on; sempre exige toggle + prompt do SO |
media.video.max_height |
1080 | Cap em 4K só se Settings.advanced ativo (raras as redes) |
media.video.bitrate_mbps |
5 | 5 Mbps casa com 1080p30 H.264 sem visible blocking |
Quando qualquer toggle for OFF em runtime, o app deve parar qualquer capture ativo + liberar handles + dismiss screen-share dialog.
3 — MUST: privacidade + recording indicator
- NUNCA auto-upload de gravação (user confirma antes do POST)
- NUNCA screen-capture em background sem user-visible indicator
- NUNCA keep camera open em background além de 5 s pós-leave
- Recording indicator obrigatório: dot vermelho + timer + título
da janela alterado para
● Recording — <app>; em mobile, system status bar fica vermelho (iOS) / mostra ícone (Android) - EXIF/metadata strip equivalente para vídeo (camera serial, GPS
de container MP4);
media.video.strip_metadata = truedefault
4 — MUST: widget surface no koder_kit
| Widget | Função |
|---|---|
KoderVideoPlayer |
Playback de KoderVideoRef ou URL; controls config (playpauseseekfullscreencaptions/speed) |
KoderVideoRecorder |
Gravação com preview + start/stop + flip-camera; gates media.video.camera |
KoderScreenCapture |
Screen share dialog + capture pipeline; gates media.video.screen; força recording indicator |
KoderVideoPicker |
Sheet "Gravar vídeo" / "Escolher da galeria"; retorna KoderVideoRef |
KoderVideoThumbnail |
Frame thumbnail (default 1s mark) + lazy-load |
KoderVideoRef: {uri, mime, durationMs, width, height, sizeBytes,
hasAudio, codec, koderUserId?, workspaceId?}.
4.1 — MUST: contrato de estado dos controles de transporte (state machine)
O KoderVideoPlayer obedece ao mesmo contrato normativo de transporte de specs/media/audio.kmd §4.1 (R1–R7) — idle → playing ⇄ paused → completed, com R1 como piso: Pause a partir de playing DEVE ir a paused preservando positionMs, NUNCA reiniciar/recomeçar (é o mesmo bug recorrente em players rolados à mão). Resume continua de onde parou; Stop zera; Seek mantém o estado; fim → completed. Vale igual pra vídeo (não só áudio).
Regras adicionais específicas de vídeo (Rn testáveis):
| # | Regra |
|---|---|
| V1 | em paused, o player exibe o frame da posição atual (não tela preta, não o primeiro frame) |
| V2 | toggle fullscreen preserva positionMs e o estado (playing continua playing, paused continua paused) — NUNCA reinicia |
| V3 | trocar caption/legenda, speed ou faixa de áudio não muda positionMs nem o estado |
| V4 | rotação de tela / resize não reinicia o playback (mantém posição + estado) |
Teste obrigatório (headless, no widget): R1 (pause preserva posição) + V2 (fullscreen preserva posição) são o piso. Este contrato DEVE ser gerado como TDD automaticamente e rodar como gate de release de todo componente Koder com player de vídeo (Drive, kall, etc.), via a categoria media-control do /k-test (projects/koder-stack#185). Testar uma vez no KoderVideoPlayer cobre todos os consumidores (headless-first + reuse-first) — por isso o widget compartilhado é mandatório (hoje cada app rola o próprio → o bug recorre).
5 — MUST: format support
Decode (via engines/kodec): H.264, HEVC (HEIC pipeline), AV1, VP9 — container MP4, WebM, MOV.
Encode (default): H.264 + AAC + MP4 container. HEVC e AV1 são opt-in quando suportado pela surface (iOS encoder, browser MediaRecorder com codec hint).
Upload size cap: 500 MB pós-compress. Exceder → erro <APP>-VIDEO-SIZE-001 com sugestão de bitrate menor ou trim.
Compression knob: media.video.bitrate_mbps (§2) controla output; duração + resolução + bitrate determinam tamanho. App deve estimar size pré-encode e avisar se ultrapassar cap.
6 — MUST: error surface
| Cenário | ID | Texto pt-BR |
|---|---|---|
| Permissão de câmera negada | <APP>-VIDEO-CAM-001 |
"Permita o acesso à câmera para gravar vídeo." |
| Permissão de mic negada | <APP>-VIDEO-MIC-001 |
"Permita o acesso ao microfone para vídeo com áudio." |
| Permissão de screen-capture negada | <APP>-VIDEO-SCR-001 |
"Permita o compartilhamento de tela para continuar." |
| Codec não suportado pelo decoder local | <APP>-VIDEO-CDC-001 |
"Este vídeo usa codec não suportado ($codec)." |
| Tamanho excede 500 MB | <APP>-VIDEO-SIZE-001 |
"Vídeo muito grande. Reduza qualidade ou corte trechos." |
| Upload falhou | <APP>-VIDEO-NET-001 |
"Falha ao enviar o vídeo. Tente novamente." |
| Encode falhou (kodec/hardware) | <APP>-VIDEO-ENC-001 |
"Falha ao processar o vídeo. Tente novamente ou reduza qualidade." |
7 — Observability
- Counters:
media.video.recorded,media.video.uploaded,media.video.upload_error,media.video.screen_captured - Latency histograms:
media.video.encode_ms,media.video.upload_ms,media.video.first_frame_ms(playback) - Não emitir métricas que vazem conteúdo (transcript, frames,
file path) — apenas counters + latência + dimensões anonimizadas (resolução, codec, duration)
8 — Adoption checklist (per app)
- [ ] Importa
KoderMediaSettingsTile(sub-seção Vídeo visível) - [ ] Usa
KoderVideoPlayerKoderVideoRecorderKoderScreenCapture(nuncavideo_playerupstream raw) - [ ] Defaults da §2 respeitados em fresh install
- [ ] Recording indicator obrigatório (§3)
- [ ] Codec layer via
engines/kodec(ou Koda port quando flipped) - [ ] Error map da §6 implementado
- [ ] Upload path respeita
multi-tenancy/contract.kmd
Non-normative — referências
- Sibling specs:
specs/media/image.kmd(camera shared),specs/media/audio.kmd(mic + recording indicator paralelo),specs/media/document.kmd - Codec engine:
engines/kodec/(canônica, Rust); Koda port emengines/lang/koda/lib/{mp4,mp3,wav}.kd(shadow-active per Flipping Point em CLAUDE.md) - Implementation surface:
engines/sdk/koder_kit/lib/src/media/ - Voice precedent (mic + privacy contract):
specs/voice/wake-word.kmd