Cross platform webcam usage
- Node.js
>=20.9.0.
| Platform | Built-in/native path | Optional fallback requirements | Notes |
|---|---|---|---|
| Linux | Bundled native V4L2 prebuild for linux-x64 |
ffmpeg, then fswebcam |
Other Linux architectures/libc variants fall back when no matching native prebuild exists. |
| macOS | None yet | ffmpeg, then imagesnap |
Camera permissions are controlled by macOS and may require granting access to the terminal/app running Node.js. |
| Windows | None yet | ffmpeg with an explicit DirectShow device, then CommandCam.exe via setup |
Run npx @anthonylzq/node-webcam setup windows for CommandCam fallback, or set NODE_WEBCAM_COMMANDCAM_PATH. |
Version 3.0.0 is a semver-major release line. It intentionally does not
publish the new capture API as 2.x because several public APIs changed:
| 2.x behavior | 3.x replacement |
|---|---|
Factory root export |
Use create(options) from the package root. |
create(options, type) platform/backend override |
Use create(options); backend selection is automatic and capability-aware. |
capture({ type, returnType }) or responseType |
Use capture({ location, options }); it always resolves to WebcamCaptureResult. |
| Buffer/base64 response modes | Use result.buffer or result.toBase64(). |
list(type) platform override |
Use listWebcams({ timeout, signal }) or list() for the current platform. |
Deep imports from dist/* |
Import only from @anthonylzq/node-webcam. |
The explicit root-only exports map is part of the 3.x contract. Backend
implementations, native bindings, and build output paths are internal and may
change without being exposed as supported subpaths.
Linux uses the bundled native V4L2 addon when a matching prebuild is available,
then falls back to ffmpeg when it is installed, and finally to fswebcam.
The native backend currently supports single-planar V4L2 streaming devices that
accept MJPEG at the requested resolution exactly. Linux camera listing uses the
same capability filter when the native addon is available, so multi-planar-only
devices are not advertised until the capture path supports them. That filter
uses synchronous native V4L2 open()/VIDIOC_QUERYCAP calls, so avoid running
Linux camera listing on latency-sensitive request paths if you have unreliable
camera drivers.
# Optional fallback backends
# ubuntu
sudo apt-get install ffmpeg fswebcam
# arch
# fswebcam requires a build from the AUR
yay -S ffmpeg fswebcam
macOS uses ffmpeg when it is installed, then falls back to imagesnap.
# Optional preferred backend
brew install ffmpeg
# Legacy fallback backend
# Repo https://github.com/rharder/imagesnap
brew install imagesnap
Windows uses ffmpeg when it is installed and a camera device name is
provided, then falls back to CommandCam.exe. CommandCam is installed by an
explicit setup command so package installation does not depend on lifecycle
scripts:
npx @anthonylzq/node-webcam setup windowsThe setup command downloads the CommandCam release asset on Windows, verifies
its SHA-256 checksum, and stores it in the user-local node-webcam cache. The
CommandCam release tag and checksum are pinned in package.json and only need
updates when that binary changes. Set NODE_WEBCAM_COMMANDCAM_PATH to use a
manually installed executable instead.
On Windows, prefer device names returned by listWebcams() such as
"Integrated Webcam". ffmpeg interprets device as a DirectShow device name.
CommandCam supports the same friendly names with /devname and also supports
1-based numeric indexes with /devnum. Numeric strings such as "1" are
treated as CommandCam indexes and skip ffmpeg selection. CommandCam only
supports BMP output; requests for jpeg, jpg, or png require ffmpeg and
fail clearly if the fallback would be CommandCam.
capture() and create() choose the first compatible available backend for
the current platform. The public API does not require backend selection.
flowchart LR
A["capture()"] --> B{"Platform"}
B -->|Linux| C["native:v4l2"]
C -->|unavailable| D["ffmpeg:v4l2"]
D -->|unavailable| E["fswebcam"]
B -->|macOS| F["ffmpeg:avfoundation"]
F -->|unavailable| G["imagesnap"]
B -->|Windows| H["ffmpeg:dshow"]
H -->|unavailable| I["CommandCam"]
| Platform | Selection order |
|---|---|
| Linux | native:v4l2 -> ffmpeg:v4l2 -> fswebcam |
| macOS | ffmpeg:avfoundation -> imagesnap |
| Windows | ffmpeg:dshow -> CommandCam |
Backend capability checks can change that order:
| Requested options | Eligible backends |
|---|---|
Basic snapshots (width, height, output, device, timeout, save) |
Native and ffmpeg backends are considered first when supported by the platform. |
Legacy capture options (quality, delay, frames, title, subtitle, timestamp, greyScale, rotation, topBanner, bottomBanner, skip) |
Legacy backends only: fswebcam, imagesnap, or CommandCam. |
Explicit ffmpegPath on Linux |
Skips native Linux and checks ffmpeg first. |
signal cancellation |
Uses command backends; native Linux capture is skipped because the addon does not yet accept AbortSignal. |
bmp output |
Requires a BMP-capable backend: ffmpeg on Linux/macOS/Windows or CommandCam on Windows. The fswebcam Linux fallback rejects BMP before command execution. |
Selection is re-evaluated for each create() call so camera hotplug, PATH,
environment, and permission changes are not hidden by a global cache. ffmpeg
availability is checked with ffmpeg -version; it does not open or capture from
the camera during selection. The probe has its own short timeout, even when
capture timeout is disabled. ffmpeg selection also requires a compatible
device signal: Linux devices must exist and pass the V4L2 capture-capability
filter when the native probe is available, macOS requires an explicit
AVFoundation device, and Windows numeric device strings are reserved for
CommandCam. The non-destructive ffmpeg check does not prove that a DirectShow or
AVFoundation device name exists; invalid names fail during capture and do not
trigger another backend fallback after capture has started. clearBackendCaches()
is exported for forwards compatibility and also backs the deprecated
clearBackendSelectionCache() alias.
Fallback only happens while selecting a backend. If the selected backend starts
a capture and fails because of permissions, an invalid device, a timeout, or an
unsupported format, the error is surfaced instead of silently trying the next
backend. The backend used for a capture is available as result.backend and
result.backendType.
The Linux native V4L2 backend avoids spawning a CLI for every capture, so it is usually faster for repeated webcam snapshots. Local benchmark snapshot:
| Backend | 100 captures total | Mean | Median | p95 | Avg bytes |
|---|---|---|---|---|---|
native:v4l2 |
28.92s | 289.24ms | 287.97ms | 288.72ms | 163,029 |
fswebcam |
36.16s | 361.59ms | 365.52ms | 371.97ms | 96,015 |
ffmpeg:v4l2 |
37.51s | 375.09ms | 370.60ms | 385.05ms | 59,196 |
Relative mean latency, scaled to the slowest backend in this run:
| Backend | Mean latency | Relative bar |
|---|---|---|
native:v4l2 |
289.24ms | ███████████████░░░░░ |
fswebcam |
361.59ms | ███████████████████░ |
ffmpeg:v4l2 |
375.09ms | ████████████████████ |
Benchmark environment: Linux, /dev/video0, 1280x720, 100 captures per
backend. Results vary by camera, driver, resolution, codec, CPU, and whether
the first capture is warmed up.
Reproduce locally:
npm run benchmark:capture -- --iterations=100 --warmup=3 --backends=native,ffmpeg,fswebcamThe benchmark writes JSON and Markdown reports under tmp/:
tmp/benchmark-results.json
tmp/benchmark-results.md
tmp/benchmarks/<run-id>/results.json
tmp/benchmarks/<run-id>/results.mdAll supported public APIs are exported from the package root:
import {
capture,
clearBackendCaches,
create,
defaults,
getMetricsReport,
listWebcams,
resetMetrics,
WebcamError,
type NodeWebcamConfig,
type WebcamCaptureResult
} from '@anthonylzq/node-webcam'Deep imports such as @anthonylzq/node-webcam/dist/* are intentionally not
supported. The package uses an explicit exports map so internal build layout,
native bindings, and backend implementations can change without becoming public
API.
-
The simplest use case:
import { capture } from '@anthonylzq/node-webcam' const main = async () => { const result = await capture({ location: 'picture.jpeg' }) console.log('buffer', result.buffer) console.log('base64', result.toBase64()) }
On Windows without an eligible ffmpeg device, the CommandCam fallback only supports BMP output. Use a
.bmplocation there, or install/configure ffmpeg forjpeg,jpg, orpngcaptures. -
In case you want to use another file type such as
jpg,pngorbmpyou must indicate it in theoptionsobject, otherwise you will get an error:import { capture } from '@anthonylzq/node-webcam' const main = async () => { const result = await capture({ location: 'picture.png', options: { output: 'png' } }) console.log('buffer', result.buffer) }
This is because in order to build the captured image correctly the file extension and output type must match.
-
If you only need the returned buffer and want to skip file persistence, set
savetofalse:import { capture } from '@anthonylzq/node-webcam' const result = await capture({ location: 'picture.jpeg', options: { save: false } }) console.log(result.buffer)
You can also provide a custom save handler:
await capture({ location: 'picture.jpeg', options: { save: async (path, buffer) => { await uploadSomewhere(path, buffer) return false } } })
-
In case you need something more advance you can use the
createfunction that will give you a class that will handle the usage of the webcam for you.import { create } from '@anthonylzq/node-webcam' const webcam = create()
-
In case you want to list the available cameras in your OS, you can use the
listWebcamsfunction:import { listWebcams } from '@anthonylzq/node-webcam' const cameras = await listWebcams()
-
The default configuration for all the webcams classes and methods can be found in the
defaultsobject:import { defaults } from '@anthonylzq/node-webcam' console.log(defaults) /** * { * width: 1280, * height: 720, * quality: 100, * delay: 0, * title: '', * subtitle: '', * timestamp: '', * save: true, * saveShots: true, * output: 'jpeg', * device: '', * verbose: false, * timeout: 0, * frames: 1, * greyScale: false, * rotation: 0, * bottomBanner: false, * topBanner: false, * skip: 0 * } */
-
In case you want aggregate capture metrics, you can use the optional metrics helpers:
import { getMetricsReport, resetMetrics } from '@anthonylzq/node-webcam' const metrics = getMetricsReport() console.log(metrics.captures.total) console.log(metrics.captures.averageElapsedMs) resetMetrics()
Publishing is manual. The only supported release-candidate command is:
npm run publish:checkThat command rebuilds the Linux x64 glibc native prebuild, writes
prebuilds/native-manifest.json, and runs the package validator. The validator
checks that the manifest hashes match the native sources, binding.gyp, and the
bundled .node prebuild so stale native artifacts fail before publication.
Direct npm pack runs a prepack freshness check, but it does not rebuild the
native prebuild; use it only for local inspection, not publication. Never
publish a tarball produced with npm pack --ignore-scripts because that skips
the freshness checks entirely.
- Charlie Abeling - Initial Work - Documentation - chuckfairy.
- Anthony Luzquiños - Rework - Documentation - AnthonyLzq.
