A drawn pointer grabs the frame near its top edge, drags it across the screen, lets go, then takes hold of one corner and pulls the frame to a new size. Moves ease in and overshoot slightly before settling, and the paths bow, so the motion reads as a hand rather than a tween. The resize pins the opposite corner and keeps the aspect ratio locked — a stretched video would read as broken, not as someone resizing — and clamps the width to the room left before the corner leaves the screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
86 lines
4.2 KiB
Markdown
86 lines
4.2 KiB
Markdown
# 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](#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, see how much playtime is left, and pick the background and
|
||
animation.
|
||
|
||
## Run
|
||
|
||
```sh
|
||
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, the drag animation's size range, port, and
|
||
the selectable backgrounds and animations. 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. Three 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.
|
||
- **drag** — a drawn mouse pointer travels to the frame, grabs it near its top
|
||
edge and drags it across the screen, lets go for a beat, then takes hold of
|
||
one corner and pulls the frame to a new size. The aspect ratio stays locked
|
||
and the frame stays on screen, so the resize reads as a window resize rather
|
||
than a stretch.
|
||
|
||
The queue page's **Animation** buttons pin the player to one of them, which is
|
||
how you look at a single animation without waiting for the cycle to reach it.
|
||
**Auto** (the default) hands selection back to the shuffle bag. Switching takes
|
||
effect immediately; while pinned, track changes leave the animation running.
|
||
|
||
**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. Register it under a new name in the `ANIMATIONS`
|
||
map in `motion.js` **and** in the one in `config.js` (which supplies the label
|
||
for the queue page's button), 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.
|