Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

camtint

Adjust your USB webcam's white balance, saturation, exposure and gain on macOS.

Your webcam has these controls in its firmware. macOS just never gave you a way to reach them, so most people either live with a blue-tinted picture or buy a sketchy App Store app to fix it.

camtint is a small local web widget: a live preview, sliders that write straight to the camera, and a white-card button that neutralises your colour cast in about five seconds. Changes are made in the camera, so every app sees them at once — Zoom, Meet, QuickTime, OBS — with no virtual camera in the chain and nothing running in the background eating CPU.

The camtint interface: a live camera preview on the left with a draggable sample square parked on a wall, and grouped colour, image and exposure sliders on the right.

The sample square sits on the wall behind, reading +0.0 neutral after a white-card pass. The preview is blurred purely to keep a real face out of a public README — camtint does not blur anything itself; that is just the ?blur= screenshot parameter doing its job.

Written for a CenterCam (the little middle-of-the-screen webcam, which ships with a noticeably warm cast and no software of its own), but it works with any UVC camera — which is essentially every USB webcam made.

No dependencies, no network access, no analytics. About 900 lines total.


Requirements

  • macOS (developed on macOS 26, Apple Silicon)
  • Xcode Command Line Tools, for the C compiler — xcode-select --install
  • Node.js 18+, for the local server

Install

git clone https://github.com/bornaware/camtint.git
cd camtint
./camtint

That compiles the USB helper on first run, starts a server on 127.0.0.1:7654, and opens the UI. ./camtint stop shuts it down.

Your browser will ask for camera permission — that is only for the live preview. The sliders work without it.

What you get

Sliders for whatever your camera actually supports. camtint asks the hardware what it has and shows only that, with the camera's real ranges. On the CenterCam that is white balance (2800–6500 K), saturation, hue, gamma, brightness, contrast, sharpness, gain, backlight compensation, power line frequency, exposure mode and exposure time.

Greyed-out sliders are ones the camera is currently overriding: white balance is ignored while auto-WB is on, and gain/exposure are ignored unless exposure mode is Manual. Turn the auto off and they come alive.

Neutralise on white card. Drag the sample square onto something white or neutral-grey — a wall works, so you are not holding paper up — and press the button. It sweeps the white-balance range, measures the red-vs-blue cast inside that square at each step, then refines around the best result. It usually beats auto-WB because it optimises for the patch you chose instead of the whole frame, and it does not drift when you move. The square's position and size are stored server-side in patch.json, so they survive a restart, a different browser, or a cleared browser profile.

Presets, and automatic restore when you replug. The settings live in the camera's volatile memory, so unplugging wipes them. An optional launchd agent notices the camera coming back and re-applies — by default whatever you last had, so a replug is invisible; or a named preset if you prefer.

./camtint install-agent            # restore settings on replug and at login
./camtint install-agent --ui       # also keep the widget server running from login
./camtint install-agent --open     # ...and pop the window open at login too
./camtint uninstall-agent

The colour restore and the widget are separate on purpose: you do not need the UI running for the camera to look right. --ui just means http://127.0.0.1:7654 is always live, so the widget opens instantly whenever you want it. The flags are declarative — re-running install-agent without --ui removes the UI agent again. camtint stop still stops the server (its agent does not use KeepAlive), whereas the watcher is always kept alive.

Mirror preview, on by default, so you read like a mirror rather than reversed. Preview only — other apps still receive the normal image, exactly like the self-view in any conferencing app.

Camera off (blackout) — drives exposure, gain and brightness to their minimums so every app sees black, and puts your settings back when you switch it off.

Read this before relying on it: blackout is convenience, not privacy. The sensor is still powered, still capturing, and still streaming frames — they are just black. An app that drives exposure itself (Zoom's "Adjust for low light") can undo it, and it is no defence against anything malicious. Unplugging is the only real off.

The UVC spec does define a real firmware shutter (CT_PRIVACY_CONTROL), and camtint will expose it as a proper on/off switch if your camera implements it. Most cheap webcams do not — the CenterCam advertises camera-terminal controls 0x00000a, which is auto-exposure and exposure time and nothing else. Check yours with bin/uvcctl list and look at ct_controls; bit 18 is privacy.

Blackout deliberately does not persist: the rolling snapshot is suppressed while it is on, and a replug clears it. Auto-restore will never hand you back a camera that is mysteriously black.

A CLI, if you would rather script it than click:

bin/uvcctl list                              # attached UVC cameras
bin/uvcctl caps                              # every supported control + range, as JSON
bin/uvcctl get saturation
bin/uvcctl set white_balance_temperature 5200
bin/uvcctl reset                             # factory defaults
bin/uvcctl watch                             # print a line whenever a camera appears

list reports each camera's raw pu_controls / ct_controls bitmaps, which is the quickest way to see what your hardware actually implements. --force probes a control even when the descriptor does not advertise it, for cameras that under-report.

Add -d vid:pid to target a specific camera when several are attached. Values are clamped to the camera's own reported range, so a bad number cannot wedge it — and reset always brings it back.

Will it work with my camera?

Probably. Run bin/uvcctl caps and see what comes back. Every USB webcam implements UVC, but which controls are wired up varies by model — some expose red/blue gain (white_balance_component) that the CenterCam does not, some expose focus and zoom, some skip hue.

Only reads and writes to controls the camera advertises are attempted, so an unsupported one is absent rather than broken.

How it works

Webcam controls are set over USB using the USB Video Class protocol: a control transfer to the camera's VideoControl interface, addressed to a unit inside it. macOS implements the video half of UVC but never exposed the control half, so the knobs exist on the device with no UI attached.

src/uvcctl.c opens the USB device through IOKit and sends those requests directly. The macOS video driver claims the streaming interfaces, not the device, so this works without a kext, without root, and while the camera is live in another app.

A few things that were not obvious while building it, in case you are writing something similar:

  • Auto controls gate manual ones. While auto-WB is engaged, or exposure mode is anything but Manual, the camera silently discards writes to the values it owns — no error, the write just does nothing. Restoring a preset therefore takes three passes: drop the gates to manual, write the manual values, then restore the gates. Any other order half-applies and looks like a hardware bug.
  • GET_INFO is 0x86 and GET_DEF is 0x87. Transposing them makes most controls vanish from the capability probe for no visible reason.
  • For enum controls GET_RES returns a bitmap of supported modes, not a step size. That is how camtint knows which exposure modes to offer.
  • Do not use launchd's LaunchEvents / com.apple.iokit.matching to react to a USB device. It fires, but launchd expects the job to consume the XPC event stream it delivers; a shell or Node job cannot, so launchd decides the event went unhandled and relaunches the job every 10 seconds, forever. It looks like it is working while quietly rewriting your camera all day. camtint registers its own IOKit arrival notification instead and blocks at 0% CPU.
  • launchd gives jobs a bare PATH with no Homebrew, so a job that shells out to node will not find it.

Alternatives

Worth knowing about, because they may suit you better:

  • CameraController — a proper native macOS app with a menu bar UI. The most polished option by a distance; use it if you want sliders and nothing else.
  • VVUVCKit — an Objective-C framework, if you are building your own app.
  • uvc-util — a long-standing CLI.

camtint exists because none of them do the white-card neutraliser or restore settings automatically when the camera is replugged, and because a browser UI made the live preview and colour sampling nearly free.

Troubleshooting

"no UVC camera found" — check bin/uvcctl list. If it is empty, the camera is not enumerating; try another port or cable.

A slider does nothing. It is probably gated — see whether an auto toggle above it is on, or exposure mode is not Manual.

Settings vanished. The camera was power-cycled: unplugged, moved to another port, or the Mac slept with the port unpowered. That is what install-agent is for.

Settings change during a call. Some conferencing apps drive UVC controls themselves — Zoom's "Adjust for low light" moves exposure and gain out from under you. Turn that off, or re-apply your preset after joining.

The agent is not restoring. Check restore.log. If you moved the folder, re-run ./camtint install-agent — the agent stores an absolute path.

Taking a screenshot? Add ?blur=14 to the URL to soften the preview — handy if you would rather not publish your face or your room, as in the screenshot at the top of this page. It is a display filter only; the colour sampler still reads the raw frame, so neutralise is unaffected.

Layout

camtint              launcher: start / stop / install-agent / apply / restore
server.js            HTTP API + static host, no npm dependencies
public/index.html    the UI
src/uvcctl.c         UVC control transfers over IOKit
Makefile             builds bin/uvcctl

License

MIT — see LICENSE.

About

Adjust your USB webcam's white balance, saturation and exposure on macOS — built for the CenterCam, works with any UVC camera. The controls macOS never exposed.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages