Skip to content

feat: add R2 video processing and project reel - #136

Merged
HazAT merged 18 commits into
masterfrom
chore/planning-workflow-scope
Aug 11, 2026
Merged

feat: add R2 video processing and project reel#136
HazAT merged 18 commits into
masterfrom
chore/planning-workflow-scope

Conversation

@HazAT

@HazAT HazAT commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Add resumable private R2 uploads with Workflow/FFmpeg processing and normalized MP4 playback.
  • Add project video management and a resilient, resumable screening reel.

Verification

  • npm run verify
  • npm audit --omit=dev

HazAT added 12 commits August 11, 2026 16:52
Replace the Stream direct-upload lifecycle with durable R2 multipart sessions, immutable queued submissions, active-project database constraints, and confirmed retirement that retains stored objects.

Add browser-side part streaming and persisted resume metadata, remove attachment promotion and Stream job endpoints, and cover authorization, concurrency, expiry, idempotency, and retention in local tests.
Start one deterministic, attempt-fenced Cloudflare Workflow when an R2 multipart upload completes, and route scoped source/output access through a Container outbound handler without exposing bucket credentials.

Add a digest-pinned multi-architecture FFmpeg image that probes, rejects overlong or malformed input, performs two-pass -16 LUFS normalization, supplies deterministic silence, and emits immutable H.264/AAC fast-start MP4 derivatives. Configure local concurrency one and prepared production concurrency two, with real Docker and local Workflow smoke coverage.
Serve current canonical R2 derivatives through authenticated same-origin endpoints with exact single-range responses and storage-neutral playback descriptors. Keep curated ordering while adding project team overlay data without exposing unrelated user fields.\n\nReplace HLS attachment with direct MP4 playback, embed ready videos on project pages, and harden the double-buffered reel with one-clip preloading, active-slot advancement, recoverable errors, pause/skip, and fullscreen controls.
Replace fake readiness and direct SQL promotion with generated media uploaded through local multipart R2, processed by the real Workflow and pinned FFmpeg Container, then verified through probe, authenticated ranges, playlist inclusion, retirement, and retained bytes.\n\nAdd reproducible processor benchmarks, isolated production resource declarations capped at two, structured processing events, and an operations runbook covering approval, provisioning, smoke, observability, rollback, and retained storage. Remove superseded Stream measurement and archive surfaces, and include processor plus real-byte readiness in the complete verification gate.
Keep the legacy project_videos and stream_events schema intact during the rollout and move the new R2 lifecycle onto video_submissions. This lets both the deployed pre-release Worker and the R2 Worker operate after migration 0007, preserving a valid Worker rollback target without restoring active Stream behavior in the new release.\n\nAdd an executable populated-schema migration proof for legacy reads and writes, R2 writes and active constraints, retained data, and foreign keys. Update rollout automation and documentation to identify the safe expand/deploy/rollback sequence and require a separately approved future contraction.
Fence stale upload rows in an active expiring state before aborting their exact R2 multipart session, and release the project slot only after storage cleanup is confirmed. Treat missing uploads idempotently while preserving the fence and returning a retryable error for transient abort failures.

Lease in-flight completion attempts so expiration cleanup cannot preempt a current completion. Cover lost resume metadata, duplicate fresh creates, missing multipart state, transient failures, and completion races.
When an initial canonical transcode cannot reach the loudness target because dynamic true-peak limiting is required, re-analyze the derivative and run one audio-only corrective pass while copying the encoded video. Revalidate codec, dimensions, fast-start layout, and the existing ±0.7 LU gate after correction. Add a deterministic high-crest fixture that reproduces the rejected short-clip behavior.
Let project managers restart a failed Workflow attempt against the retained immutable original instead of retiring and re-uploading the video. Refresh the project-video query cache from the retry response and cover the recovery action in the video UI test.
Clear upload progress once the multipart upload completes, and let the inline player represent a ready video without a duplicate status card or watch-page link. Remove the storage fine print and cover the streamlined completed state in the video UI tests.
Move the existing video-removal action into the panel header and label it clearly as delete video so managers can find it without scrolling past the player. Explain in the confirmation that deletion enables a replacement upload, while preserving the existing retirement-backed storage behavior.
Include every ready project video without requiring a manual screening entry, while preserving curated projects first and enforcing closed-year member access. Add group-aware HTML interludes, full-viewport screening, play-from-here URL recovery, and automatic continuation past broken clips on top of the existing preloaded player.
Render reel entries with the same project row, group tag, and contributor stack used by the overview list, with the row title starting playback from that project. Remove the implementation-detail playback copy and the now-redundant reel-specific card styling.
Comment thread src/worker/workflows/video-processing.ts Outdated
Drop the benchmark, processor fixture runner, local readiness journey, and rollout E2E script from the pull request. Simplify the verification command and documentation accordingly, and remove the rollout runbook from the repository.
Comment thread src/app/video/upload.ts
Limit the large reel heading typography to the section heading instead of every nested project title. Keep contributor initials aligned at the end of each playlist row and add regression coverage for the shared member bubbles.
Comment thread src/app/player/controller.ts
Add a controller-backed timeline that remains usable in normal and fullscreen screening modes, without exposing native fullscreen controls that bypass HTML interludes. Fade each project overlay after its introduction and return stable member IDs in playlist data so shared project rows render the same contributor bubbles as the overview.
Comment thread src/app/player/controller.ts
Comment thread src/app/player/ScreeningPlayer.tsx
Comment thread src/worker/services/videos.ts
Keep the seek timeline available in normal playback while removing the control overlay from fullscreen screening so it never obscures project video content or HTML interludes.
Comment thread src/app/player/ScreeningPlayer.tsx
Comment thread src/worker/routes/videos.ts
Treat processor capacity as durable backpressure, close the multipart pause race, and keep screening audio and error state stable across clip transitions. Separate responsive control rows and reset the player when playlist membership changes to prevent mobile overlap and stale-index crashes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 240ff1a. Configure here.

Comment thread src/app/player/controller.ts
Treat AudioContext resume as best-effort during ended and error-driven autoplay transitions so a browser-held resume promise cannot stall the reel. User-triggered start and resume actions still await audio activation, and the controller test covers a permanently pending automatic resume.
Comment on lines 82 to 88
export async function getProjectVideo(db: D1Database, projectId: string) {
const row = await videoByProject(db, projectId);
const row = await db
.prepare(`${videoSelect()} WHERE project_id = ? AND retired_at IS NULL`)
.bind(projectId)
.first<VideoRow>();
return row ? mapVideo(row) : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Video metadata and content readable by any authenticated user, bypassing the submissions-closed gate

getProjectVideo performs no membership/ownership check, so any logged-in user can fetch the video ID for any project and then download the actual video via the unguarded /api/videos/:videoId/content endpoint, bypassing the 'screening reel is available after submissions close' restriction enforced in listPlaylist.

Evidence
  • getProjectVideo (services/videos.ts:82-88) queries by project_id and returns mapVideo(row) (which includes the video id) without calling authorizeVideoWrite or any membership check.
  • The route projectVideoRoutes.get('/:projectId/video') in routes/videos.ts passes only the path param; all /api/* routes require only authenticateRequest (index.ts:44), so any logged-in user can resolve any project's video ID (project IDs are listed to all users).
  • issuePlayback/getVideoContent (services/videos.ts:160-192) call only requireReadyVideo (status/key checks, no authz), and routes/videos.ts serves GET /api/videos/:videoId/content from R2 with no user check.
  • listPlaylist explicitly gates non-admins with 403 'The screening reel is available after submissions close' (services/videos.ts:95-104), establishing that pre-close cross-team video access is a real boundary the content path does not enforce.

Identified by Warden · security-review · 5PP-XX5

@HazAT
HazAT merged commit c787877 into master Aug 11, 2026
13 checks passed
@HazAT
HazAT deleted the chore/planning-workflow-scope branch August 11, 2026 20:01
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