Skip to content

feat(attachments): play every browser-playable video container inline + label sniffed types - #319

Merged
lopugit merged 2 commits into
developfrom
claude/screenshot-video-display-e3a72d
Aug 19, 2026
Merged

feat(attachments): play every browser-playable video container inline + label sniffed types#319
lopugit merged 2 commits into
developfrom
claude/screenshot-video-display-e3a72d

Conversation

@lopugit

@lopugit lopugit commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Why

A QuickTime-container screen recording named .mp4 (magic bytes ftypqt) uploaded to a post rendered as a generic 41.7 MiB · application/octet-stream file card instead of a video player, because the inline allowlist only admitted magic-byte-verified video/mp4 + video/webm and everything else collapsed to an unlabeled octet-stream download.

What

1. Every browser-playable video container now renders inline. ATTACHMENT_INLINE_CONTENT_TYPES (server) and the mirrored INLINE_VIDEO_TYPES (client) grow from MP4/WebM to: QuickTime (video/quicktime), M4V (video/x-m4v), Ogg (video/ogg), 3GPP/3GPP2, and Matroska (video/x-matroska). The decision still comes exclusively from server-side magic-byte sniffing at upload finalization (file-type over the first 8 KiB) — never the filename extension — and SVG/HTML/active formats remain opaque downloads. SAFE_VIDEO_CONTENT_TYPES in attachmentCore matches so canonical crystal re-derivation agrees.

2. Opaque downloads now name their real container. New optional crystal field detectedContentType preserves the sniffed MIME whenever the served contentType stays application/octet-stream (AVI, WMV, MPEG-TS, HEIC, PDF, ZIP…). The canonical crystal closed union accepts the field only alongside an octet-stream contentType, so it is display metadata that can never widen inline serving or accompany an inline type. It flows through attachmentPublicProjection → post/comment projections → the client normalizer, and the download row shows a friendly label (AVI video, PDF document, raw MIME as fallback) instead of application/octet-stream. Legacy 4-key crystals stay canonical.

3. Graceful codec fallback. Container support ≠ codec support (HEVC QuickTime on Firefox, MKV on Safari). PostAttachments now degrades a failed <video> element to its named download row via the element error event instead of leaving an inert black player.

Registration / docs

  • apiDocs.ts: upload-complete + content endpoint details and steps updated (docs registry is the route registration; no new endpoints).
  • schemas/registry.ts: attachment schema documents the optional detectedContentType field; the pinned projection in builtinSchemaProjection.test.ts updated per its review-prompt comment.
  • TESTING.md: three new manual checklist rows (QuickTime/MKV inline, sniffed-container labels, codec-failure fallback).

Tests & verification

  • npm run test:unit — green (exit 0), including 110 attachment tests with new cases: every new container finalizes inline; AVI finalizes octet + detectedContentType; canonical-crystal accepts the 5-key shape and rejects detected-on-inline, detected=octet, uppercase, and non-string forgeries; UI mediaKind/normalizer/label coverage.
  • Targeted ESLint on all changed files — clean.
  • Full production build (npm run build in remix/: Vite client + Nitro Vercel output + verify) — passes.
  • The reporter's actual file (ftypqt QuickTime bytes, 41.7 MB) was verified locally as the exact trigger: file-type reports video/quicktime, previously excluded by the allowlist.
  • Local env has no private-storage S3 config, so the end-to-end upload was not exercised locally; preview-deployment verification notes to follow on this PR.

Out of scope (follow-up)

Existing ready attachments keep their stored octet-stream crystal (finalized before this change); a backfill that re-sniffs stored objects and upgrades crystals (e.g., the reporter's original post) needs S3 access inside the admin-migration frame and is left as a follow-up. Re-uploading the file after deploy gets the new behavior. Inline audio remains deliberately disabled pending a vetted player.

🤖 Generated with Claude Code

… + label sniffed types

Widen the magic-byte inline allowlist from MP4/WebM to every video
container mainstream browsers can natively play: QuickTime (.mov —
including screen recordings misnamed .mp4), M4V, Ogg, 3GPP, 3GPP2, and
Matroska. The decision still comes only from server-side byte sniffing,
never the filename extension, and SVG/HTML/active formats remain opaque
octet-stream downloads.

When a sniffed container stays a generic download (AVI, WMV, MPEG-TS…),
preserve the sniffed MIME as a new optional crystal field
detectedContentType so the download row can say 'AVI video' instead of
application/octet-stream. The canonical crystal union accepts the field
only alongside an octet-stream contentType, keeping it display metadata
that can never widen inline serving; client normalization mirrors that.

Since codec support inside an allowed container varies per browser
(HEVC QuickTime on Firefox, MKV on Safari), a failed <video> element now
degrades to its named download row instead of an inert black player.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 19, 2026 3:01am
thingtime (develop) Ready Ready Preview Aug 19, 2026 3:01am

Request Review

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🧹 Develop S3 preview removed

The PR-specific alias and every workflow-created develop deployment were removed when this PR closed.

The ordinary generated Vercel Preview remains available on the shared development runtime.

@github-actions
github-actions Bot temporarily deployed to develop-pr-319 August 19, 2026 02:55 Destroyed
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/screenshot-video-display-e3a72d — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • remix/app/docs/apiDocs.ts

Please review the merge commit before relying on it.

…d conflicts)

Conflicted paths: remix/app/docs/apiDocs.ts

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32210305983

Co-Authored-By: Claude <noreply@anthropic.com>
@lopugit

lopugit commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Preview verification notes

Reporter's actual file, exact server decision path (real bytes, real file-type, real detectedAttachmentType):

file-type (first 8 KiB): {"ext":"mov","mime":"video/quicktime"}   ← ftypqt QuickTime container named .mp4
finalized crystal patch: {"contentType":"video/quicktime","mediaKind":"video"}   ← now renders inline

Before this PR the same bytes collapsed to application/octet-stream / file card, which is exactly the reported bug.

Preview deployment E2E (thingtime-48ftqdhxb-…vercel.app, commit b657f57):

Remaining manual check for an approved account (30 seconds): upload a macOS screen recording (or this PR's reporter file) to a post — it should finalize as video/quicktime and play inline; an AVI should show a download row labelled AVI video instead of application/octet-stream.

🤖 Generated with Claude Code

@lopugit
lopugit merged commit dfca890 into develop Aug 19, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant