The rule: film the game you will actually play
Every preview video on GameNow — the clip on a game's page and the vertical Short made from it — is recorded by playing the real game in a browser. In almost every case that is the production build at the game's own address, such as four-ball.gamenow.us or pinball.gamenow.us. There are no mockups, rendered trailers or effects added in editing. If a game can't be recorded doing something, the clip doesn't show it.
Each site preview is logged in a recordings file with its length, a SHA-256 fingerprint of the video and its poster image, and an asset type that reads 'Gameplay footage recorded by tools/shorts/record.mjs' followed by the play script that produced it. The site adds the first 12 characters of that fingerprint to the video's address, so when I re-record a game, your browser loads the new clip instead of an old cached copy.
The rule has already cost me footage. An earlier Mulkko preview showed the board for 12 seconds while nothing happened, and an earlier Four-ball Billiards preview filmed the whole page, aimed, and ended before a shot. Both were thrown out and recorded again with real solving and real shots.
A headless browser dressed as a phone
The recorder, record.mjs, opens the game in headless Chromium. For Shorts it behaves like an Android phone: a 432 × 672 CSS-pixel viewport at a device pixel ratio of 2.5, with touch enabled, which gives 1080 × 1680 frames. Site previews use a 1280 × 720 desktop window at 1.5×, encoded at 1920 × 1080, and are played with keyboard and mouse.
A few things are changed before recording, and only inside that browser. The first-visit guide is marked as already seen. Full-screen requests are ignored, because a sudden change of window size mid-recording squashes the frames. The portal's menu and share buttons are hidden, since they are not part of the game. Hiding them means injecting a style that the site's content security policy would normally block, so the recording browser alone skips that policy.
Scripts that actually play
Each game has a play file with two parts: setup, which runs before the camera starts (menus, mode selection), and run, the 15 to 25 seconds that get recorded. The scripts use practice or guest modes that leave nothing on leaderboards, and they act through the same inputs a player has: taps, pointer drags and key presses.
Four-ball Billiards is the most involved. The game's shot physics live in one TypeScript file, physics.ts, which both the game and its AI opponent use. The recorder bundles that same file into the page and reads the ball positions from the 3D scene. On my turn it simulates up to about 550 candidate shots: 19 cut angles at five strengths against each red ball, banks off all four cushions, and a full circle of 120 directions. A shot that scores is worth far more than one that merely touches a red, and fouls are penalised. The best candidates are then nudged by ±0.003 and ±0.006 radians and by 1.5% of the power gauge; the script prefers shots that still score after those nudges, and softer ones, so the balls stop rolling before the next turn. The chosen shot is confirmed once more with a full simulation.
Then it plays the shot the way you would. It drags across the table to aim, taps Shift+← or Shift+→ to fine-tune the cue in the game's 0.05° steps until the angle matches, and pulls the power gauge on the right down to the chosen strength. The opponent is the game's own Normal AI, and the target score is set to its maximum so the match can't end and show a results screen mid-clip.
NEON ORBIT, the pinball table, runs on a deterministic engine: 120 physics ticks per second, three sub-steps per tick, and every random event drawn from a seed. The same seed with the same inputs produces the same game, which is also how the server replays ranked runs. For the preview, the recording browser rewrites the page's app.mjs as it loads, swapping in a fixed seed and a small flipper bot that returns an input mask each tick. The bot holds the plunger for 120 ticks (one second) and releases it, then lifts a flipper whenever a ball that isn't heading back up enters the strip just above it. I ran the same bot in Node across thousands of seeds and chose one where bumper combos, missions, multiball and a jackpot all land in the first 30 seconds. When the table was redesigned on 2026-10-08, the preview was re-recorded with seed 430 instead of 2827; the clip runs 26.1 seconds.
Other scripts use each game's own data or tools. The Two Beat script reads the chart and presses F and J against the song time you hear, 8 ms early so the average error sits near zero. Mulkko's clips follow the minimum solutions from Mulkko's solver. Some scripts deliberately vary their timing, because an even tempo looked mechanical. Mini Golf takes a rough first pull, corrects it and pauses before letting go; Mulkko speeds up on repeated moves and hesitates when it changes direction. In Mini Golf the final release is still exactly the computed shot.
Sharp frames without stutter
Playwright's built-in video recording captures at CSS pixels, so a phone recording came out at 432 × 672 and looked soft. The recorder uses the Chrome DevTools Protocol screencast instead, which delivers JPEG frames at quality 88 and full device resolution.
Screencast frames arrive only when the screen changes, so their timing is irregular. An earlier version stitched them with ffmpeg's concat list and a duration per frame. Those durations were rounded to 1/25 of a second, which made every clip freeze and then skip once every six frames; a test recording of a moving bar made the hitch obvious. Now, for each 1/30-second slot, the recorder takes the last frame that had arrived by that moment (allowing half a slot ahead), lays the frames out as a numbered sequence, and encodes it at a fixed 30 fps with x264 at CRF 18.
Heavy 3D games had a separate problem: at 2.5× this M1 Mac could draw only about 25 frames per second, so a 30 fps video still hitched every sixth frame. Those games' play files lower the device scale, and the encoder scales the result back up to 1080 wide with a Lanczos filter.
Recording the game's own sound
To capture sound, the recorder patches the Web Audio API before the page loads. Every AudioContext gets a hidden second output, and every connection to the speakers is copied there. When recording begins, each output is captured with MediaRecorder as Opus audio in 250 ms chunks, and a context created later is captured from the moment it appears. This matters because the portal's sound effects and a game engine often use separate audio contexts.
Each track is shifted or trimmed by the gap between its start time and the first video frame, then the tracks are mixed. Loudness differed widely between games — around −48 dB for puzzle sound effects, −17 dB for the rhythm game — so Shorts are normalised to −14 LUFS and site previews to −18 LUFS.
Waiting for a quiet machine
A busy computer makes a browser game stutter, and the stutter ends up in the video. Clips recorded on 2026-10-05 while the load average was 144 were unusable. Since then the recorder waits until the one-minute load average drops below the number of CPU cores. Only one recording runs at a time, enforced with a lock file; a lock older than ten minutes is treated as a crashed run and removed. After each recording the tool reports the frame rate, and below 15 frames per second it warns that the clip should be redone.
Keeping recordings out of the numbers
Recording sessions must not look like players. The recorder blocks analytics and tracking requests, the site's own event endpoints, and the challenge and visit records a game sends when a round ends. The scripts choose modes that write nothing to rankings: Four-ball against the local AI, NEON ORBIT as a guest practice table, and Two Beat with its record and challenge requests also aborted.
A few games need a server or a login before they can be recorded. For those, a separate script starts local copies of the game servers with no production accounts, saves or scores. It can point at an archive of the last git commit, so half-finished edits in my working folders never appear in a public clip.
Framing for 16:9 and 9:16
Site previews are landscape. preview.mjs scales the desktop recording to 1280 × 720 and saves a 960 × 540 poster frame. It can crop away a page header, cut out dead time and join the remaining segments, or lightly denoise a game with heavy film grain — on UFO the grain made the file more than ten times larger.
Shorts are 1080 × 1920. The 1080 × 1680 phone recording fills the frame under a 240-pixel band carrying the game's name, and a 2.5-second end card fades in over the frozen last frame, so the play itself is never cut short. If a game has only a landscape recording, it sits in the middle over a blurred copy of itself. Games with several modes, such as Two Beat's three, are joined from separate recordings with a label on each part.
What the footage does not show
Being straight about the method also means listing what it leaves out.
- The player is a script. Its timing is tuned to look human, but it never hesitates the way a person does.
- Moments are chosen. NEON ORBIT's seed was picked for a busy first half-minute, and Two Beat's Short opens on the densest stretch of Neon Jump Rope on Hard, filmed after an unrecorded run-up that built a combo.
- Interface around the game is hidden. In the real game you will also see the portal menu, share and settings buttons, and Four-ball's coaching hints.
- In DOGFIGHT and MECH MARS the game's own autopilot or demo pilot does the piloting, and the player's craft can't be destroyed during recording, so the clip doesn't end early.
- The hooks exist only in the recording browser. If the production code the pinball hook rewrites ever changes, the hook stops with an error instead of quietly recording something different.
Why this matters when you press Play
A preview is a promise about what the game looks, sounds and plays like. Because the clips come from the same build you open, the table, the physics, the graphics and the sound in the video are the ones you get. When NEON ORBIT's table was redesigned on 2026-10-08, its preview was recorded again the same day instead of being left on the old table.