A Chrome extension that scrolls YouTube Shorts and Instagram Reels when you open your mouth. Raise your eyebrows to go back. Your hands never move. Neither do you, really.
The Story
It started, as these things do, at an angle. I was lying sideways on my bed, phone propped somewhere, laptop open, one arm folded under my head and the other one doing the only work anyone was doing that evening: flicking up. Next Short. Next Short. Next Short.
At some point the arm started to complain. Genuinely. Twenty minutes of thumb work and my wrist wanted a word. And instead of the reasonable thought — maybe stop watching Shorts — I had the other one: what if I didn't have to move at all?
Not the keyboard. Not the mouse. Not the trackpad. Not even a finger. Just me, horizontal, staring at a screen, and the videos keep coming. The face was already pointed at the laptop. The webcam was already there. The face was, technically, free real estate.
So: open your mouth, next video. Raise your eyebrows, previous video. That's the whole product. I built the laziest possible input device and then spent a very unlazy number of hours making it work.
The irony is not lost on me. To avoid moving one thumb, I wrote roughly five hundred lines of face-tracking JavaScript, hand-generated the icon files byte by byte, and debugged a state machine for eyebrows. Worth it. I now watch Shorts like a Roman emperor being fed grapes, except the grapes are algorithmically selected and my expression is doing all the labour.
The Whole Interface
Two gestures. What they do depends on what's on screen — the overlay tells you which mode you're in.
SHORTS & REELS
MOUTH → next video
BROWS → previous video
LONG YOUTUBE VIDEOS
MOUTH → play / pause
BROWS → hold to skip +5s
Every trigger also plays a short tone through the Web Audio API — a falling note for next, a rising one for previous — so you know it registered without looking away from the video.
How It Actually Works
The content script grabs a 320×240 webcam stream and runs face-api.js — TinyFaceDetector to locate the face, then FaceLandmark68TinyNet to place 68 landmark points on it. Both are the tiny variants, because this is running alongside a video player and nobody wants their laptop fan competing with the audio.
Mouth openness is the gap between landmark 51 and 57 (top lip to bottom lip) divided by the distance between the two outer eye corners. Brow raise averages all five brow points per side against the upper eyelids, over the same eye-corner span. Dividing by eye span is the entire trick: lean toward the screen and every raw pixel measurement doubles, but the ratio doesn't move.
Each gesture is a tiny state machine: CLOSED → OPEN → CLOSED is what counts as one trigger, not "mouth is currently open". Otherwise a yawn scrolls thirty videos. On top of that sits a shared cooldown — 1.5s by default — so mouth and brows can't stampede each other.
On YouTube it looks for the real Shorts next/previous buttons and clicks them, falling back to a synthetic ArrowDown/ArrowUp keydown if the markup changed. On long videos it skips the DOM entirely and sets video.currentTime directly. On Instagram it fires the key event at document, body and window, because Reels only listens on one of them and which one depends on the day.
The Build
Four moving parts, all plain JavaScript. No React, no TypeScript, no bundler, no npm install — the dependency count is genuinely zero, and face-api.js plus its model weights are committed straight into the repo so the thing runs from an unzipped folder.
content.js — the actual product. Camera, detection loop, both state machines, the draggable overlay panel with the live camera preview and two indicator dots, and the code that clicks YouTube's buttons.
background.js — service worker. Seeds default settings and tells the content script when the tab's URL changed.
popup.* — the toolbar panel: on/off, mouth sensitivity, brow sensitivity, cooldown, camera preview toggle. Everything persists through chrome.storage.sync, and the content script reacts to changes live instead of needing a reload.
gen_icons_node.js — writes the PNG icons by hand with Node's zlib, no image library involved. The dot-matrix mouth mark at the top of this page comes out of a sibling script the same way.
Problems I Had To Solve
Turning it off didn't turn it off
The service worker wrote its defaults on onInstalled, which sounds like "first install" and absolutely is not — it also fires on every extension update and every reload of an unpacked extension. So each reload silently flipped MouthScroll, and the camera, back ON after I'd turned it off. Now it only fills in keys that have never been set.
YouTube never actually loads a page
Going from a Short to a regular video is a pushState call, not a navigation — the content script never reruns, so the gestures kept doing the Shorts thing on a long video. Fixed from both ends: the extension patches history.pushState and replaceState to notice URL changes, and the service worker also reports them over messaging as a backstop.
Sitting closer changed the gesture
A threshold in pixels only works at one distance from the laptop. Lean in and a neutral face reads as a wide open mouth. Normalising both measurements against eye-corner distance made the thresholds hold up whether you're upright at a desk or, more realistically, folded into a beanbag.
Skipping ahead needed a different kind of gesture
Raise-and-lower works for a discrete action like "previous video", but seeking through a long video that way is exhausting. On /watch the brows switch to a held gesture: keep them up and it seeks +5s every 500ms, rate-limited separately from the main cooldown, so holding a face is the same as holding down a key.
Where The Camera Feed Goes
Nowhere. There is no server, no account, and nothing to opt out of — detection runs inside your own browser and no frame, image, or measurement ever leaves the machine. The extension can only reach youtube.com and instagram.com, the camera only runs on a supported page while it's switched on, and the only thing stored anywhere is your slider settings. It felt worth being strict about: an extension that watches your face is exactly the kind of thing that shouldn't be quietly interesting about it.