Media — Video record, playback, screen-capture, formats (privacy + widgets)

mandatory

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* em engines/sdk/koder_kit (Flutter) e koder_web_kit (JS) — nunca rolar video_player upstream ou <video> raw local. Codec layer via engines/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:

  1. Câmera (media.video.camera) — gate à camera
  2. Microfone para vídeo (media.video.mic) — gate ao mic durante gravação
  3. Compartilhar tela (media.video.screen) — gate ao screen-capture
  4. Resolução máxima (media.video.max_height) — dropdown 480 / 720 / 1080 / 2160
  5. 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 = true default


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 (nunca video_player upstream 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 em

    engines/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