The idea

Patrons can send a photo or short video from pub.ihnyc-rc.org/up. A DJ normally reviews it, then the media briefly fills the projector before the regular visual resumes.

flowchart LR
  PHONE["Patron phone<br/>pick and preview"] --> UPLOAD["Upload"]
  UPLOAD --> REVIEW{"DJ review<br/>unless auto-approve is on"}
  REVIEW -- Reject --> GONE["Remove media"]
  REVIEW -- Approve --> READY["Prepare for projector"]
  READY --> WALL["Show briefly on wall"]
  WALL --> HISTORY["Keep history and media"]

Operators use /admin/dj-mode to review patron uploads, remove approved items, upload their own media, choose the display duration, and control the rest of DJ Mode.

What the patron sees

The /up page has three simple states:

  1. pick a photo or video;
  2. preview it and confirm Send to the wall;
  3. receive success or a retryable error.

The Worker independently checks the file type and enforces a 50 MB maximum. It recognizes common phone image and video containers even when an in-app browser reports an empty or generic MIME type.

MediaPreparation
JPEG, PNG, WebP, GIFStored directly for the wall
HEIC, HEIF, AVIF, BMP, TIFF, or another non-native imageConverted to JPEG with Cloudflare Images; rejected if a projector-safe copy cannot be made
MP4, MOV, WebMConverted to a silent, projector-safe H.264 MP4 before playback

Moderation and auto-approve

The safe default is moderation. flash:config.autoApprovePatron in DJ_FLASH KV changes only that decision:

flowchart TD
  NEW["New upload"] --> AUTO{"Auto-approve?"}
  AUTO -- No --> PENDING["D1 status: pending"]
  PENDING --> DJ{"DJ decision"}
  DJ -- Reject --> REJECTED["D1 status: rejected<br/>R2 media deleted"]
  DJ -- Approve --> PROCESS["queued image or<br/>transcoding video"]
  AUTO -- Yes --> PROCESS
  PROCESS --> SHOWN["D1 status: shown"]

Auto-approved items retain source: patron-auto, so history can distinguish them from DJ uploads. Videos still have to finish processing; auto-approve does not bypass media conversion.

The DJ can also select up to 20 files from the admin page. Each file is accepted or rejected independently, so one unsupported item does not fail the rest of the batch.

Video processing

Every accepted video enters D1 with status: transcoding. The projector skips that row until a safe MP4 exists.

flowchart LR
  RAW["Raw phone video in R2"] --> EDGE["Cloudflare Media<br/>8-second silent H.264 MP4"]
  EDGE -- Success --> QUEUED["D1 status: queued"]
  EDGE -- Unavailable or failed --> WAIT["Short grace period"]
  WAIT --> PI["On-site Pi + ffmpeg fallback"]
  PI -- Success --> QUEUED
  PI -- Failure or 10-minute timeout --> FAILED["D1 status: failed<br/>source preserved"]

Cloudflare Media gets the first attempt. If it is unavailable or fails, the on-site dj-flash-transcoder polls for work, downloads the source, runs ffmpeg, and posts the MP4 back. The Pi uses outbound requests only; it does not expose an inbound service.

Relevant machine endpoints are Bearer-protected with the same ingest credential used by the Pub Pi:

EndpointPurpose
GET /api/dj-flash/pending-transcodesList eligible D1 rows waiting for the Pi fallback
POST /api/dj-flash/transcoded/:idUpload the finished MP4 and mark the row queued
POST /api/dj-flash/transcode-failed/:idMark the row failed without repeatedly retrying it

Queue and display

The durable queue is the D1 flash_uploads table, not a JSON array in KV. Atomic row inserts prevent two simultaneous phones from overwriting one another.

StatusMeaning
pendingWaiting for DJ review
transcodingApproved video still being prepared
queuedReady for the wall
shownPlayed on the wall
rejectedDJ rejected it before approval
dequeuedDJ removed it after approval
failedMedia preparation failed or timed out

DJ_FLASH KV now holds only short-lived coordination values: the active item, the last-finished time used to space flashes, and the auto-approve configuration.

The /dj-now-playing projector listens to the DJ Mode SSE stream. It selects the oldest ready item, skips anything still transcoding, and displays the media for its chosen duration. The default is eight seconds, with a minimum gap between consecutive flashes.

Storage and retention

R2 (DJ_VISUALS) stores the bytes under these lanes:

patron/pending/<uuid>.<ext>             awaiting review
patron/pending/processed/<uuid>.mp4     safe preview for a pending video
patron/approved/<uuid>.<ext>            approved patron media
crowd/<uuid>.<ext>                      DJ or auto-approved media
crowd/<uuid>.mp4                        final safe video

D1 holds the state, file key, MIME type, source, size, duration, timestamps, and audit request metadata. Current upload handling records the request IP address and user agent on the flash_uploads row.

Current retention behavior

Automatic deletion of shown /up media is disabled. Shown media and failed-transcode sources are preserved; an explicit reject or dequeue removes the associated R2 object. The public privacy-policy wording for this flow is under human review in docs issue #50.

Operator checks

An upload did not appear

  1. Check Pending moderation when auto-approve is off.
  2. Check the item’s D1 status in the DJ Mode history.
  3. For transcoding, verify Cloudflare Media and the dj-flash-transcoder service.
  4. A row left transcoding for ten minutes becomes failed; its source remains available for diagnosis.
  5. Check the projector console only after the row is queued.

Something inappropriate was submitted

  • Reject a pending item to delete it.
  • Dequeue an approved item to remove it before display.
  • Turn auto-approve off immediately; items already accepted remain in their current state.

There is no patron block-list UI in the current code. Do not improvise one from request metadata in public documentation or chat.

Sources

  • ihnyc-avi-pub-landing/worker.js — upload, moderation, Media, SSE, and Pi endpoints
  • ihnyc-avi-pub-landing/worker/lib/flash-uploads-store.js — D1 lifecycle and history
  • ihnyc-avi-pub-landing/migrations/0026_flash_uploads.sql — durable queue
  • ihnyc-avi-pub-landing/migrations/0051_dj_flash_media_retention_off.sql — retention change
  • ihnyc-avi-pub-landing/pi/dj-flash-transcoder/main.py — Pi fallback
  • ihnyc-avi-pub-landing/public/up/index.html and public/dj-now-playing/index.html — patron and projector surfaces