Inspiration
I run the 400 and the 800 at Skyline, in Sammamish. The split I get after a race is when a parent on the rail taps a screen. That tap is late. Sometimes it is early. It is never the body on the line. I wanted the time when I hit the mark. One phone on a tripod can already see one plane. FinishLynx trucks do not show up at a Tuesday dual. So I built a finish camera that sits on one mark and tells you which mark that is. The geometry of a 400m oval is the constraint, not a footnote. The 200 and the finish are opposite sides. One lens cannot see both. Split does not pretend it can.
What it does
Split is a finish-line camera for any runner. Live camera or an uploaded clip. You draw a yellow plane on the video. ARM takes a one-second empty-line baseline. After that, a body crossing the plane stamps the split. The clock is huge. ARM is one fat control. Manual MARK exists and stays small on purpose. Default stock preset: 800 at FINISH, empty goal. First crossing is the 400. Second is the finish. Type your own goal if you want even-split math. Anyone can add events, marks, split names, planned crossings, calibrate window, and debounce. Share the setup as a URL or as JSON. Times are $M{:}SS.xx$. There is no built-in coach. Connect AI is bring-your-own: a webhook that POSTs each crossing, a session JSON download, or an optional OpenAI-compatible endpoint. Keys stay in this browser.
How I built it
Vanilla JavaScript. No npm. No frameworks. Static files. Serve with python3 -m http.server or GitHub Pages.
getUserMedia feeds the stage. A click places a vertical or horizontal plane. Each frame copies the video into an offscreen canvas, maps the on-screen line through object-fit: cover into video pixels, and reads 64 luma samples along that strip:
$$
L = 0.299R + 0.587G + 0.114B
$$
ARM calibrates an empty-line baseline $B$ for one second. Energy is mean absolute error against that profile:
$$
E = \frac{1}{64}\sum_{i=1}^{64} \lvert L_i - B_i \rvert
$$
A crossing fires when $E \ge \tau$ for 2 consecutive frames (default $\tau = 18$), then the line ignores new hits for $800\,\mathrm{ms}$. Quiet frames update the baseline with a slow EMA ($\alpha = 0.08$) so a cloud does not become an athlete. The plane flashes white when it stamps.
If you type a goal $G$ with $N$ planned crossings and $k$ already down,
$$
\mathrm{even} = \frac{G}{N},\qquad
\mathrm{remaining} = G - t,\qquad
\mathrm{next} = \frac{G - t}{N - k}.
$$
Optional MediaPipe Pose (CDN) draws stick figures. A hip that crosses the plane can also stamp. If the CDN is blocked, luma still works.
Challenges I ran into
file:// blocks the camera. The page has to say so and print the server command.
A full-height average washes out a small body, so detection is a 64-point profile, not one pixel.
ARM has to calibrate before the gun. If calibrate eats the first second of the race, the first split is a lie.
object-fit: cover means the visible line is not the video line. Mapping that wrong double-stamps or misses. Tick marks on the plane were the only way the line read as a mark instead of a UI decoration.
Opposite sides of a 400m track cannot share one lens. I almost designed a two-mark story. Runners would have known it was fake.
Accomplishments that I'm proud of
The crossing is the product. The clock is the size of a meet board. The UI says which mark the camera is on. Stock events are editable. The demo works with a hand waved through the line, no meet, no account, no API key.
What I learned
A finish camera is a start watch plus a line. Honesty about what one phone can see matters more than a longer feature list. If you lie about seeing the 200 and the finish, the people who run those races will know.
What's next
A second phone on the other side, talking over a local link. Not in this submission.
Built With
- css
- getusermedia
- html
- javascript
Log in or sign up for Devpost to join the conversation.