Every time I needed to check whether my microphone actually worked before a call, or whether a key on a used keyboard was dead, I ended up on some sketchy "free online tester" that wanted an account, showed six ad banners, and — the worst part — happily streamed my webcam to a server I knew nothing about.
So I built the thing I actually wanted: TestOnDevice — 118+ device tests that run 100% in the browser. No installs, no account, and nothing you record ever leaves your machine.
Here's what I learned building it.
The core constraint: the network is off-limits
The single rule that shaped the whole project: no device data touches a server. No webcam frames, no audio buffers, no keystrokes, no sensor readings.
That constraint turned out to be freeing rather than limiting, because modern browser APIs are genuinely powerful. Almost every "device test" is just a Web API plus a bit of rendering:
| What you're testing | The API doing the work |
|---|---|
| Microphone / speakers |
MediaDevices.getUserMedia, Web Audio (AnalyserNode) |
| Webcam |
getUserMedia, <video>, MediaStreamTrack.getSettings()
|
| Keyboard |
keydown / keyup, KeyboardEvent.code
|
| Mouse / touch / stylus | Pointer Events, PointerEvent.pressure
|
| Gamepads | Gamepad API |
| MIDI keyboards | Web MIDI API |
| Display (dead pixels, refresh rate) | Fullscreen + requestAnimationFrame
|
| Motion / orientation |
DeviceOrientationEvent, DeviceMotionEvent
|
A live mic meter, entirely local
The microphone test is a good example of how little you need. Grab a stream, wire it into an AnalyserNode, and compute RMS on each frame for a level meter — no recording, no upload:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
ctx.createMediaStreamSource(stream).connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
function tick() {
analyser.getByteTimeDomainData(data);
let sum = 0;
for (const v of data) {
const x = (v - 128) / 128;
sum += x * x;
}
drawMeter(Math.sqrt(sum / data.length)); // RMS -> level bar
requestAnimationFrame(tick);
}
tick();
The most important line, though, is the cleanup. The moment the user stops or leaves, the hardware indicator light should go off:
stream.getTracks().forEach((track) => track.stop());
Releasing tracks aggressively is what makes a diagnostics tool feel trustworthy instead of creepy.
Gamepads: no events, only polling
The Gamepad API surprised me. You get a gamepadconnected event, but after that there are no button events at all — you have to poll the current state every frame:
window.addEventListener("gamepadconnected", (e) => {
console.log("connected:", e.gamepad.id);
});
function poll() {
for (const pad of navigator.getGamepads()) {
if (!pad) continue;
pad.buttons.forEach((b, i) => b.pressed && highlight(i));
// Analog sticks: pad.axes[0], pad.axes[1], ...
}
requestAnimationFrame(poll);
}
poll();
That polling model is exactly why it's great for detecting stick drift: read pad.axes while the sticks are untouched and watch whether the values sit at zero or quietly wander. If you want to see it raw, the controller input viewer is just a live JSON dump of every button and axis.
The permissions gotcha that bites everyone
enumerateDevices() will happily list your cameras and microphones before you grant permission — but the label field is an empty string until you do:
const devices = await navigator.mediaDevices.enumerateDevices();
// device.label is "" until the user grants permission to that kind of device
This is a deliberate anti-fingerprinting measure. The UX fix is simple: show generic entries ("Microphone 1", "Camera 1") first, then re-enumerate and reveal real names after the user opts in. I ended up building a whole device enumerator around exactly this behavior.
Privacy as an architecture, not a promise
"We respect your privacy" in a footer is worthless. What actually earns trust:
- No backend for device data. There's no endpoint to upload frames to, so there's nothing to leak.
- Late permission requests. Access is only ever asked for after an explicit click on that specific test.
- Immediate teardown. Streams are released on stop and on page unload.
- No account, no fingerprint. Nothing to correlate you across sessions.
Because there's no server round-trip, most tests also work as a PWA offline — handy on a fresh machine with no drivers yet installed.
Takeaways
If you're building anything hardware- or media-adjacent for the web:
- Reach for platform APIs before libraries —
getUserMedia, Gamepad, Web MIDI, Pointer Events and the sensor APIs cover an enormous amount of ground. - Treat stream cleanup as a first-class feature, not an afterthought.
- Let real privacy fall out of the architecture. "No server" is a stronger guarantee than any policy page.
You can try the whole thing here: testondevice.com. Start with the Quick Check if you just want a fast pass over everything, or browse all tests — mic, camera, keyboard, mouse, gamepad, MIDI, dead-pixel and refresh-rate — all running locally in your tab.
If you want a deep dive into any single one (the refresh-rate detector and the dead-pixel finder both have some fun edge cases), let me know in the comments and I'll write it up.
United States
NORTH AMERICA
Related News
Building Production-Grade LLM Evaluation Pipelines: From Vibes to Metrics
13h ago

I built a vector topographic contour map generator for designers (SVG export)
13h ago
The future of development is full-stack
6h ago
Effatà: Chronicle of a Human-AI Collaboration for a Different Kind of Search Engine by DeepSeek, AI assistant
3h ago
Resolving color contrast over CSS gradients
20h ago