feat: video support on postcards (upload + inline playback) #7

Closed
opened 2026-06-16 14:20:53 +00:00 by mAi · 2 comments
Collaborator

What

Add video as a media type on postcards, alongside the existing images. m wants to attach a video to a card and have it play on the rendered postcard.

MVP scope (keep it tasteful + simple, like the image flow)

  • Compose: allow attaching a video (in addition to / instead of images). Mobile-friendly file picker, same opt-in feel as image upload.
  • Storage: upload to Supabase Storage (same postcards bucket, e.g. a video/ key prefix), same pattern as images. Store a postcard_videos row (or extend postcard_images/a media table) — mirror the existing ordered-media model.
  • Render: inline HTML5 <video controls playsinline> on the card, mobile-first. Works in the gallery layout and (sensibly) the flip-card — pick the cleanest interaction; don't let video controls fight the swipe/flip/lightbox gestures already in place.
  • Format: accept mp4 (H.264/AAC — broadest mobile support); webm optionally. Validate type + size before upload, like images.

Key constraint — file size / storage

Videos are large. The single most important decision:

  • Set a sensible max upload size (e.g. 50 MB) and reject larger with a friendly message — same validation shape as images (MAX_UPLOAD_BYTES).
  • Confirm the Supabase Storage instance's object-size limit allows it (check the bucket/global limit on the shared msupabase before assuming 50MB works; adjust the cap to what's actually allowed).
  • No server-side transcoding in MVP — accept what the phone produces (mp4), play as-is. Note for later: a poster-frame/thumbnail would be nice but is out of MVP unless it falls out cheaply.

Constraints (carry over from the app)

  • Mobile-first, German UI + Umlaute, noindex on card pages.
  • Renamable: nothing hardcodes the domain (read from BASE_URL).
  • Schema-ownership: any new migration runs as the postgres role via the pooler (NOT supabase_admin) — see #1/#2/#3 history, or the deploy crash-loops.
  • Coexists with everything shipped: single/gallery/flip layouts, broadcast mode, custom Shlink slugs, image lightbox.

DoD

  • Compose lets you attach a video; it uploads to Supabase Storage; the rendered /c/{token} card plays it inline on mobile + desktop.
  • Size/format validation with friendly errors.
  • Cards without video unchanged. build/vet/test green; e2e verified on a real card (upload a small mp4, play it); test data cleaned. Commit references this issue. otto-head merges + deploys to post.msbls.de.
## What Add video as a media type on postcards, alongside the existing images. m wants to attach a video to a card and have it play on the rendered postcard. ## MVP scope (keep it tasteful + simple, like the image flow) - **Compose**: allow attaching a video (in addition to / instead of images). Mobile-friendly file picker, same opt-in feel as image upload. - **Storage**: upload to Supabase Storage (same `postcards` bucket, e.g. a `video/` key prefix), same pattern as images. Store a `postcard_videos` row (or extend `postcard_images`/a media table) — mirror the existing ordered-media model. - **Render**: inline HTML5 `<video controls playsinline>` on the card, mobile-first. Works in the gallery layout and (sensibly) the flip-card — pick the cleanest interaction; don't let video controls fight the swipe/flip/lightbox gestures already in place. - **Format**: accept `mp4` (H.264/AAC — broadest mobile support); `webm` optionally. Validate type + size before upload, like images. ## Key constraint — file size / storage Videos are large. The single most important decision: - Set a sensible **max upload size** (e.g. 50 MB) and reject larger with a friendly message — same validation shape as images (`MAX_UPLOAD_BYTES`). - Confirm the Supabase Storage instance's object-size limit allows it (check the bucket/global limit on the shared msupabase before assuming 50MB works; adjust the cap to what's actually allowed). - No server-side transcoding in MVP — accept what the phone produces (mp4), play as-is. Note for later: a poster-frame/thumbnail would be nice but is out of MVP unless it falls out cheaply. ## Constraints (carry over from the app) - Mobile-first, German UI + Umlaute, `noindex` on card pages. - Renamable: nothing hardcodes the domain (read from BASE_URL). - **Schema-ownership**: any new migration runs as the `postgres` role via the pooler (NOT supabase_admin) — see #1/#2/#3 history, or the deploy crash-loops. - Coexists with everything shipped: single/gallery/flip layouts, broadcast mode, custom Shlink slugs, image lightbox. ## DoD - Compose lets you attach a video; it uploads to Supabase Storage; the rendered `/c/{token}` card plays it inline on mobile + desktop. - Size/format validation with friendly errors. - Cards without video unchanged. build/vet/test green; e2e verified on a real card (upload a small mp4, play it); test data cleaned. Commit references this issue. otto-head merges + deploys to post.msbls.de.
mAi self-assigned this 2026-06-16 14:20:53 +00:00
Author
Collaborator

Video support implemented & e2e-verified

Commit: 340dd7e491
Branch mai/hermes/issue-7-feat-video (otto-head merges + deploys).

What shipped

  • Compose: optional video file input (mp4/webm) next to the photo picker, same opt-in feel, with a live client-side preview.
  • Storage: uploaded to the same postcards bucket under a <token>/video/ key prefix; public URL derived at render time (so a BASE_URL / backend swap needs no data migration). Mirrors the image flow.
  • Schema: migration 004_videos.sql adds postcards.postcard_videos — mirrors postcard_images exactly (storage_key / content_type / position, ON DELETE CASCADE). Additive + idempotent; a fresh CREATE runs as the postgres role via the pooler, so the v1 ownership-crash class (#1/#2/#3) can't recur. Verified: the migration ran cleanly against the live shared schema during e2e and does not break the currently-running old binary (extra unused table).
  • Render: inline <video controls playsinline preload="metadata"> on the card, mobile-first. In gallery and single it sits above the images; on the flip card it renders in its own block above the scene so its native controls never fight the swipe / flip / lightbox gestures already in place. The lightbox only wires .postcard img, so videos are untouched.
  • Validation: container sniffed from magic bytes (mp4 ftyp box / webm EBML header) rather than the client-supplied Content-Type (which doesn't reliably recognise webm). Friendly German errors for wrong type and oversize.

Key constraint — storage size limit (verified, not assumed)

Probed the shared msupabase Storage instance directly: the postcards bucket has file_size_limit: null (inherits the global), and the global ceiling is exactly 50 MiB52428800 bytes uploads OK, 52428801HTTP 413 "object exceeded the maximum allowed size". So MAX_VIDEO_UPLOAD_BYTES defaults to 50 MiB (= the real ceiling). Going higher would need the instance's global storage limit (and any proxy body limits in front of supa.flexsiebels.de) raised first. No server-side transcoding (out of MVP); a poster-frame is noted as a future nicety.

New env (documented in .env.example)

  • MAX_VIDEO_UPLOAD_BYTES (default 52428800 = 50 MiB)
  • MAX_VIDEOS (default 1)

e2e (real production path — shared DB + Supabase Storage)

  • Composed a card with a real H.264/AAC mp4 → rendered inline player with correct src + type="video/mp4"; the stored object served byte-identical with Content-Type: video/mp4, ffprobe confirmed a playable h264+aac mp4. ✔️
  • webm (vp9) accepted and rendered with type="video/webm". ✔️
  • Non-video upload → HTTP 400 + „Nur MP4- oder WebM-Videos werden unterstützt.“ ✔️
  • Test data cleaned: both test cards + storage objects deleted; 0 postcard_videos rows remain.

Quality

  • Cards without a video render unchanged (covered by tests).
  • gofmt clean, go vet, go build, go test ./... all green. New tests: detectVideoType magic-byte sniffing, per-layout <video> render (and absence when no video), compose video field, config defaults.
  • Carries over all app constraints: mobile-first, German UI + Umlaute, noindex, nothing hardcodes the domain. Coexists with single/gallery/flip, broadcast mode, custom Shlink slugs, image lightbox.

Deploy note for otto-head: ensure the reverse proxy in front of post.msbls.de (Traefik) allows request bodies up to ~131 MiB (8×10 MiB images + 50 MiB video + slack) so large uploads aren't truncated at the edge.

## ✅ Video support implemented & e2e-verified **Commit:** https://mgit.msbls.de/m/postcards/commit/340dd7e491c2dd3ffb358765267df4001ed7db6c Branch `mai/hermes/issue-7-feat-video` (otto-head merges + deploys). ### What shipped - **Compose:** optional video file input (mp4/webm) next to the photo picker, same opt-in feel, with a live client-side preview. - **Storage:** uploaded to the same `postcards` bucket under a `<token>/video/` key prefix; public URL derived at render time (so a BASE_URL / backend swap needs no data migration). Mirrors the image flow. - **Schema:** migration `004_videos.sql` adds `postcards.postcard_videos` — mirrors `postcard_images` exactly (`storage_key` / `content_type` / `position`, `ON DELETE CASCADE`). Additive + idempotent; a fresh `CREATE` runs as the `postgres` role via the pooler, so the v1 ownership-crash class (#1/#2/#3) can't recur. Verified: the migration ran cleanly against the live shared schema during e2e and does **not** break the currently-running old binary (extra unused table). - **Render:** inline `<video controls playsinline preload="metadata">` on the card, mobile-first. In **gallery** and **single** it sits above the images; on the **flip** card it renders in its own block *above* the scene so its native controls never fight the swipe / flip / lightbox gestures already in place. The lightbox only wires `.postcard img`, so videos are untouched. - **Validation:** container sniffed from **magic bytes** (mp4 `ftyp` box / webm EBML header) rather than the client-supplied `Content-Type` (which doesn't reliably recognise webm). Friendly German errors for wrong type and oversize. ### Key constraint — storage size limit (verified, not assumed) Probed the shared **msupabase** Storage instance directly: the `postcards` bucket has `file_size_limit: null` (inherits the global), and the global ceiling is **exactly 50 MiB** — `52428800` bytes uploads OK, `52428801` → `HTTP 413 "object exceeded the maximum allowed size"`. So `MAX_VIDEO_UPLOAD_BYTES` defaults to **50 MiB** (= the real ceiling). Going higher would need the instance's global storage limit (and any proxy body limits in front of `supa.flexsiebels.de`) raised first. No server-side transcoding (out of MVP); a poster-frame is noted as a future nicety. ### New env (documented in `.env.example`) - `MAX_VIDEO_UPLOAD_BYTES` (default `52428800` = 50 MiB) - `MAX_VIDEOS` (default `1`) ### e2e (real production path — shared DB + Supabase Storage) - Composed a card with a real **H.264/AAC mp4** → rendered inline player with correct `src` + `type="video/mp4"`; the stored object served byte-identical with `Content-Type: video/mp4`, `ffprobe` confirmed a playable h264+aac mp4. ✔️ - **webm** (vp9) accepted and rendered with `type="video/webm"`. ✔️ - Non-video upload → `HTTP 400` + *„Nur MP4- oder WebM-Videos werden unterstützt.“* ✔️ - **Test data cleaned**: both test cards + storage objects deleted; 0 `postcard_videos` rows remain. ### Quality - Cards without a video render unchanged (covered by tests). - `gofmt` clean, `go vet`, `go build`, `go test ./...` all green. New tests: `detectVideoType` magic-byte sniffing, per-layout `<video>` render (and absence when no video), compose video field, config defaults. - Carries over all app constraints: mobile-first, German UI + Umlaute, `noindex`, nothing hardcodes the domain. Coexists with single/gallery/flip, broadcast mode, custom Shlink slugs, image lightbox. **Deploy note for otto-head:** ensure the reverse proxy in front of `post.msbls.de` (Traefik) allows request bodies up to ~131 MiB (8×10 MiB images + 50 MiB video + slack) so large uploads aren't truncated at the edge.
Author
Collaborator

Video live on https://post.msbls.de

Merged 340dd7e (merge 158f856), deployed. Compose now has an optional Video (MP4/WebM, bis 50 MB) field; the rendered card plays it inline via HTML5 <video>, mobile-first, alongside images/layouts.

Verified live e2e: uploaded a test mp4 → stored at …/postcards/{token}/video/0.mp4 in Supabase Storage, card renders the <video> player, object serves 200 video/mp4. Test data cleaned. Migration 004 (postcard_videos, owner-safe) applied cleanly. go build/vet/test green.

Note: Traefik streams request bodies (no buffering cap on post.msbls.de), so the larger upload works — verified. Not closing — m closes.

## ✅ Video live on https://post.msbls.de Merged `340dd7e` (merge `158f856`), deployed. Compose now has an optional **Video (MP4/WebM, bis 50 MB)** field; the rendered card plays it inline via HTML5 `<video>`, mobile-first, alongside images/layouts. **Verified live e2e:** uploaded a test mp4 → stored at `…/postcards/{token}/video/0.mp4` in Supabase Storage, card renders the `<video>` player, object serves 200 `video/mp4`. Test data cleaned. Migration 004 (`postcard_videos`, owner-safe) applied cleanly. go build/vet/test green. Note: Traefik streams request bodies (no buffering cap on post.msbls.de), so the larger upload works — verified. Not closing — m closes.
mAi closed this issue 2026-06-23 15:48:02 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/postcards#7
No description provided.