simonandClaude Opus 5 ce1a6d6a47 Cycle the frame through animations, add a corners effect
The frame had exactly one behaviour, started once and run forever. Make
animations a start(context) => stop() lifecycle held in a registry, and
draw a new one on every completed crossfade.

Selection uses a shuffle bag: each animation comes up exactly once per
pass, so they stay evenly used, and a swap at the pass boundary keeps the
same one from running twice in a row. Written for a registry larger than
today's two entries.

The new corners animation parks the frame in a random corner and drops a
screenshot of it into the next of the other three corners every 5s, going
clockwise. The frame's own corner is never a slot, so a still cannot
cover it by construction rather than by clamping. This holds while
FRAME_SCALE stays under 0.5, now noted where that knob lives.

Screenshots need getDisplayMedia: the decks are cross-origin YouTube
iframes, so canvas taints and there is no other way to read the playing
pixels. The prompt has to be requested before start() yields, or the
click's transient activation is gone. Declining, or serving over plain
HTTP where mediaDevices is undefined, leaves the frame parked without
stills instead of failing.

The capture sink is a 1x1 element in the document, like #probe: detached
video elements are never composited and get throttled hard enough to make
every screenshot seconds stale.

startBounce also gains the stop() it always needed — it was leaking its
rAF loop and resize listener, and a queued frame would fight whichever
animation came next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
2026-08-09 22:04:33 +02:00

Party Player

A YouTube playlist player for parties. Two browser windows run side by side:

  • Player (http://localhost:3000/) — fullscreen poster background with a small video frame that moves around, switching animation on every track (see Animations). Consecutive videos overlap in a 5 second audio/video crossfade, so there is never silence between tracks.
  • Queue (http://localhost:3000/queue) — add videos by URL, drag to reorder, delete, and see how much playtime is left.

Run

npm install
npm start

Put the player on the projector/TV screen (F11 for fullscreen) and open the queue on a laptop or phone. On the same network, other devices reach both pages at http://<this-machine-ip>:3000.

The player page opens behind a "Start" button. Browsers block unmuted autoplay, so the first click is what unlocks audio. One click per session.

Notes

  • No YouTube API key is needed. Titles come from YouTube's public oEmbed endpoint; durations are measured by a hidden muted player on the player page, which is why a freshly added video shows –:–– for a moment (and keeps showing it while the player page is closed). Totals are prefixed with ≥ whenever a length is still unknown.
  • The queue is saved to queue.json on every change and restored at startup.
  • Settings live in config.js: overlap length, frame size, drift speed, corner screenshot interval and margin, port, and the selectable backgrounds. Anything the browser needs must also be mirrored in the /config.js route in server/index.js.

Animations

The video frame's movement changes with every track. Two animations ship today:

  • bounce — diagonal drift, reflecting off the screen edges.
  • corners — the frame parks in a random corner and every 5 seconds a screenshot of it appears in the next of the three other corners, going clockwise. The frame never moves, so a screenshot can never cover it.

The corners animation asks to share your screen. The decks are cross-origin YouTube iframes, so nothing on the page can read their pixels — capturing the tab and cropping out the frame is the only way to photograph what is playing. The prompt appears once, on the first click of Start; choose this tab. Declining is fine: the frame still parks in its corner, just without the screenshots. The prompt is skipped entirely on a plain-HTTP origin (i.e. when you open the player from another machine over the network), for the same reason.

Adding animations

#frameMover carries only the motion transform (see public/player/motion.js). #frameSkin inside it is the cosmetic layer, and the two video decks sit inside that. New effects should nest a fresh wrapper rather than sharing a transform with the bounce loop.

An animation is start(context) => stop(), where context is { element, capture } — capture may be null, and is read late, so an animation must cope with it being absent. stop() has to undo everything: timers, listeners, and any DOM it added, because the next animation takes over the same element immediately. Add it to the ANIMATIONS array and it joins the rotation.

Selection uses a shuffle bag (createPicker): every animation comes up exactly once per pass, so they stay evenly used, and the same one never runs twice in a row — including across the seam between two passes.

S
Description
No description provided
Readme
91 MiB
Languages
JavaScript 87.6%
CSS 10%
HTML 2.4%