# Video Tape POST Feature - Implementation Complete ## Status: ✅ Phases 1-4 Complete, Phase 5 Documented Implementation Date: October 17, 2025 --- ## Overview This feature allows users to post video recordings (tapes) from the `video` piece, uploading them to cloud storage, creating database records with short codes, and eventually syncing to ATProto with MP4 videos. ## Architecture Flow ``` User clicks POST ↓ video.mjs (Frontend) - POST button UI ↓ create-and-post-tape message bios.mjs (BIOS Layer) - ZIP creation (6x scaled frames + timing.json + metadata.json) ↓ Upload ZIP to S3/DO Spaces ↓ POST to api/track-tape track-tape.mjs (Netlify Function) - Generate code, insert MongoDB, queue MP4 ↓ MongoDB: { code, slug, when, bucket, user, nuked, mp4Status } ↓ Return { code, slug } bios.mjs - Send tape:posted callback ↓ video.mjs - Show success message ↓ [Async MP4 Conversion Service] - Convert ZIP to MP4 ↓ POST to api/tape-mp4-complete tape-mp4-complete.mjs (Webhook) - Update MongoDB with MP4 URL ↓ Call createTapeOnAtproto() tape-atproto.mjs - Sync to ATProto with video blob ↓ MongoDB: { ..., mp4: "https://...", mp4Status: "complete", at: { rkey, uri, cid } } ↓ Media Edge Function - Route /media/{userId}/{slug}.mp4 requests ↓ Check mp4Status ↓ If complete: redirect to MP4 ↓ If processing: return HTML/JSON status page ``` --- ## Phase 1: Frontend POST Button ✅ **File**: `/system/public/aesthetic.computer/disks/video.mjs` ### Changes: 1. **State Variables** (lines 35-42): - Added `postBtn`, `isPostingTape` - Removed `btn`, `framesBtn`, `gifBtn` 2. **POST Button UI** (lines 222-225): ```javascript postBtn = new TextButton("POST", { right: 6, bottom: 6 }); ``` 3. **POST Handler** (lines 530-607): - Sends `create-and-post-tape` message with frames array - Shows progress (1% → 90%) - Disables button during upload 4. **Callbacks** (lines 1071-1107): - `tape:posted`: Show success with code - `tape:post-error`: Show error message 5. **Old Buttons Removed** (lines 428-990): - MP4, GIF export handlers commented out --- ## Phase 2: BIOS ZIP Creation & Upload ✅ **File**: `/system/public/aesthetic.computer/bios.mjs` ### Changes: 1. **create-and-post-tape Handler** (lines 5119-5310): - Receives frames array from video.mjs - Scales frames 6x using canvas (10-75% progress) - Creates ZIP with: - `frame-{n}.png` - All scaled frames - `timing.json` - Frame durations and timestamps - `metadata.json` - Piece info, dimensions, duration - Generates filename: `{piece}-{timestamp}.zip` - Calls `receivedUpload()` with metadata 2. **receivedUpload Updates** (line 12196, 12310-12355): - Added `metadata` parameter - Detects tape uploads: `ext === "zip" && metadata` - POSTs to `api/track-tape` with `{ slug, ext, metadata }` - Extracts `code` from response - Sends `tape:posted` callback with code **ZIP Structure**: ``` wand-1729177200000.zip ├── frame-0.png (3840x2880, 6x scaled) ├── frame-1.png ├── ... ├── timing.json [{ frame, filename, duration, timestamp }, ...] └── metadata.json { piece, frameCount, totalDuration, scale, ... } ``` --- ## Phase 3: Backend MongoDB ✅ **File**: `/system/netlify/functions/track-tape.mjs` ### Features: 1. **Code Generation**: - Uses `customAlphabet` (3-char codes) - Collision detection with retry logic - Expands to 4 chars after 100 collisions 2. **MongoDB Record**: ```javascript { code: "a3x", slug: "wand-1729177200000", when: new Date(), bucket: "user-aesthetic-computer" | "art-aesthetic-computer", user: "auth0|...", // undefined for guests nuked: false, mp4Status: "pending" | "processing" | "complete" } ``` 3. **Indexes**: - `{ code: 1 }` - unique - `{ user: 1 }` - sparse (only for authenticated) - `{ when: 1 }` - chronological - `{ slug: 1 }` - lookups - `{ slug: 1, user: 1 }` - unique per user 4. **Guest Support**: - Works without authentication - Stores in `art-aesthetic-computer` bucket - No `user` field (undefined) 5. **MP4 Conversion Queue**: - Calls external service with ZIP URL - Sets `mp4Status: "processing"` - Fire-and-forget (doesn't block response) --- ## Phase 4: ATProto Integration ✅ ### File: `/at/lexicons/computer/aesthetic/tape.json` **Lexicon Schema**: ```json { "lexicon": 1, "id": "computer.aesthetic.tape", "record": { "required": ["slug", "code", "when", "video"], "properties": { "slug": { "type": "string" }, "code": { "type": "string" }, "when": { "type": "string", "format": "datetime" }, "video": { "type": "blob", "accept": ["video/mp4"], "maxSize": 52428800 }, "ref": { "type": "string" } } } } ``` ### File: `/system/backend/tape-atproto.mjs` **Function**: `createTapeOnAtproto(database, mongoId, mp4Url)` **Process**: 1. Fetch tape from MongoDB by `_id` 2. Skip if guest tape (no user) 3. Check user has ATProto account 4. Download MP4 from URL 5. Login to ATProto PDS 6. Upload MP4 as blob 7. Create ATProto record with video blob 8. Update MongoDB with `at: { rkey, uri, cid }` **Error Handling**: - Returns `{ error }` for guests, no-account, failures - Logs all steps for debugging - Gracefully skips ATProto if not applicable ### File: `/system/netlify/functions/tape-mp4-complete.mjs` **Webhook Handler**: - Receives POST from conversion service - Payload: `{ mongoId, mp4Url, status: "complete" }` - Updates MongoDB: `{ mp4: url, mp4Status: "complete" }` - Calls `createTapeOnAtproto()` to sync - Returns 200 with success/skip message --- ## Phase 5: MP4 Conversion & Routing ✅ Documented ### File: `/system/netlify/edge-functions/media.js` **Enhanced Media Router**: 1. Detects `.mp4` requests matching tape pattern 2. Calls `handleTapeMp4Request(resourcePath)` 3. Queries tape status from MongoDB 4. Routes based on `mp4Status`: - **complete**: Redirect to actual MP4 URL - **processing**: Return HTML status page or JSON - **pending**: Return pending status **HTML Status Page** (202 response): ```html
Tape a3x is being processed...