npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/camera

v1.27.0

Published

Declarative camera capture and media recording for Web Components. Binds live MediaStream handles directly to elements (never through serializable state) via getUserMedia + MediaRecorder.

Readme

@wcstack/camera

🤖 AI coding agents: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository README and AGENTS.md, then use the wcstack-app skill.

Declarative camera capture (<wcs-camera>) and media recording (<wcs-recorder>) for Web Components, built on getUserMedia + MediaRecorder. Framework-agnostic, zero runtime dependencies, exposed through the wc-bindable protocol.

日本語版は README.ja.md

The idea: a live handle that never touches state

Every other @wcstack IO node moves serializable values in and out of state. A MediaStream is different — it is a live, non-serializable resource handle: reference identity is all that matters, it never "settles", and leaking it is physically observable (the camera indicator stays on).

So this package keeps the live stream out of state entirely:

  • <wcs-camera> owns a <video> preview in its shadow root and assigns srcObject internally — the handle never crosses the state boundary.
  • For other consumers (a recorder, an external <video>), the stream is published via the wcs-camera:stream-ready event-token, and handed on as a command-token argument — it passes through the token bus transiently and is never written to a reactive path.
  • Only derived values live in state: active, permission, the recorded Blob, an object URL, etc.
<wcs-camera data-wcs="
  command.start: $command.camStart;
  eventToken.streamReady: gotStream;
  active: camActive; permission: camPerm"></wcs-camera>

<wcs-recorder data-wcs="
  command.attachStream: $command.feed;
  command.start: $command.recStart;
  command.stop: $command.recStop;
  recording: recording; objectURL: clipUrl;
  eventToken.recorded: onRecorded"></wcs-recorder>
$commandTokens: ["camStart", "feed", "recStart", "recStop"],
$eventTokens: ["gotStream", "onRecorded"],
$on: {
  // The raw MediaStream is forwarded as a command argument — never stored.
  gotStream: (state, e) => state.$command.feed.emit(e.detail),
  // The recorded Blob is a value — it may live in state.
  onRecorded: (state, e) => { state.clipBlob = e.detail.blob; },
}

<wcs-camera>

Acquires a camera stream and renders a preview. Acquisition is explicitstart() (or the autostart attribute) prompts; merely connecting does not.

Attributes: facing-mode (user/environment), device-id, audio (opt the microphone in), width, height, autostart, keep-alive (do not suspend on page-hidden — set while recording).

Commands: start(), stop(), switchCamera() (toggle front/back).

Bindable values: active (a stream is live), permission / audioPermission (prompt/granted/denied/unsupported), deviceId, devices, error, errorInfo (WcsIoErrorInfo | null — a serializable failure taxonomy derived from error, published via wcs-camera:error-info-changed; see Notes & gotchas below).

Events (event-token): streamReady (wcs-camera:stream-ready, detail = the live MediaStream), error, ended (a track was revoked by the OS). The streamReady "property" exists for event-token wiring only — never bind it as a value.

Lifecycle

  • On disconnectedCallback every track is stopped (track.stop()), clearing the hardware indicator. Leaking a stream is the one failure mode unique to this node.
  • Moving the element in the DOM (remove → re-append) runs disconnectedCallback (dispose, stop tracks) then connectedCallback (observe again). With autostart it re-acquires on reconnect (and may re-prompt). To keep a stream across a move, avoid autostart and re-start() yourself, or don't detach the element.
  • A constraints change (device-id, facing-mode, switchCamera()) re-acquires (stop → new getUserMedia), guarded by a generation counter so a superseded acquire cannot leave an orphan stream live.
  • While the page is hidden the stream is suspended and re-acquired on return — unless keep-alive is set. Bind keepAlive: recording to keep the camera alive while recording.

<wcs-recorder>

Records a borrowed stream received via attachStream (the direct channel from a camera's stream-ready). It never owns or stops the stream — that is the camera's job.

Attributes: mime-type, timeslice (emit dataavailable chunks on this interval; omit for one Blob on stop), audio-bits, video-bits.

Commands: attachStream(stream), start(), stop(), pause(), resume().

Bindable values: recording, paused, duration (ms — see note below), mimeType (the resolved recording type, which may differ from the requested mime-type attribute or be filled in when none was requested), blob, objectURL, error, errorInfo (WcsIoErrorInfo | null — a serializable failure taxonomy derived from error, published via wcs-recorder:error-info-changed; see Notes & gotchas below).

Events (event-token): recorded (wcs-recorder:recorded, detail = { blob, objectURL, mimeType, duration }), dataavailable (only in timeslice mode), error.

duration is finalized at stop/pause, not live. There is no internal ticking timer: duration stays 0 from start() until the first pause() or stop(). For a live elapsed counter while recording, drive your own client-side timer off the recording flag.

mimeType has two sides — request vs. resolved. The input is the mime-type attribute (what you ask the recorder to use). The output is the mimeType bindable value (what the browser actually picked, published via wcs-recorder:mimetype-changed). They share a base name by design but are distinct surfaces: bind the attribute to set the request (mime-type attribute / element setter), and bind the value property to read the resolved type. Don't expect reading mimeType to echo back what you wrote — it reflects the recording, not the request.

The assembled Blob is structured-clone friendly, so it is a value and may flow through state — for example new File([blob], "clip.webm") into @wcstack/upload. The object URL is managed: the previous one is revoked before a new clip and on disconnectedCallback (dispose).

objectURL lifetime is bound to the recorder. Because dispose revokes the last object URL — and a new recording revokes the previous clip's URL before minting the next — any <video src> / <wcs-upload> still pointing at an old URL breaks once the <wcs-recorder> is removed or the next clip completes. Always follow the latest objectURL / recorded value; never pin a stale one. If you hand the URL to a longer-lived consumer, either keep the recorder mounted for as long as the URL is in use, or build your own URL from the Blob (URL.createObjectURL(blob)) and own its revoke. The structured-clone-friendly blob has no such coupling — prefer flowing the Blob through state and minting URLs at the point of use.

CSS styling with :state()

<wcs-camera> and <wcs-recorder> reflect their boolean output states onto ElementInternals CustomStateSet, so you can style them directly from CSS with the :state() pseudo-class — no data-wcs binding or extra class toggling required.

| Element | State | On when | |---------|-------|---------| | wcs-camera | active | wcs-camera:active-changed fires with true (cleared on false) | | wcs-camera | error | wcs-camera:error fires with a non-null detail (cleared on null) | | wcs-recorder | recording | wcs-recorder:recording-changed fires with true (cleared on false) | | wcs-recorder | paused | wcs-recorder:paused-changed fires with true (cleared on false) | | wcs-recorder | error | wcs-recorder:error fires with a non-null detail (cleared on null) |

permission / audioPermission have no boolean derived getter today, so they are not reflected (v1 scope; see docs/custom-state-reflection-design.md §7). duration is a continuous value and is intentionally excluded.

wcs-camera:state(active) ~ .live-badge     { display: block; }
form:has(wcs-camera:state(error)) .banner  { display: block; }

wcs-recorder:state(recording) ~ .rec-dot   { animation: blink 1s infinite; }
wcs-recorder:state(paused) ~ .rec-dot      { animation: none; opacity: .4; }

Unlike attributes or classes, :state() cannot be written from outside the element, so there is no risk of confusing this output state with an input.

Browser support (:state(x) syntax): Chrome/Edge 125+, Safari 17.4+, Firefox 126+. In older browsers the states are simply never set — :state() selectors never match, but the elements themselves keep working normally (graceful degradation, never-throw).

SSR: :state() cannot be serialized into HTML, so server-rendered markup never carries these states on first paint (@wcstack/server is unaffected). If you need to style the pre-hydration gap, pair your rule with wcs-camera:not(:defined) / wcs-recorder:not(:defined) instead.

Debugging

Custom states are invisible in DevTools' Elements panel and attachInternals() cannot be called twice, so there is no console way to inspect them directly. Two debug-only aids are provided for that:

  • el.debugStates — a snapshot array of the currently-on state names (e.g. ["active"]). It is not part of wc-bindable (not a bind target) and its shape is not a guaranteed contract — use it for debugging only.

  • The debug-states attribute (opt-in, default off) mirrors state changes onto data-wcs-state-* attributes on the element, so the Elements panel highlights them as they toggle:

    <wcs-camera autostart debug-states></wcs-camera>

Write your CSS against :state(), not data-wcs-state-*. The mirrored attributes exist purely to make state changes visible while debugging with DevTools open; they are not a supported styling hook.

Headless cores

CameraCore and RecorderCore are exported for non-DOM use (bind() from @wc-bindable/core). The Shells are thin wrappers.

The structural Core surface is normative across wcstack IO nodes (async-io-node-guidelines §3.9); to bind it into signals with no element at all, see @wcstack/signals — Binding a Core directly.

Notes & gotchas

  • Secure context (https) required. getUserMedia is unavailable on file:// / plain http://.

  • The camera indicator = a leak detector. If it stays on after you are done, a track was not stopped.

  • User gesture. Some browsers require getUserMedia to be triggered by a user action; firing it from a timer may silently fail (surfaced via error, never thrown).

  • Errors are classified, never thrown: NotAllowedError (denied), NotFoundError (no device), NotReadableError (in use by another app), OverconstrainedError.

  • errorInfo — additive failure taxonomy. Alongside error, both <wcs-camera> and <wcs-recorder> expose an additive bindable output errorInfo (WcsIoErrorInfo = a stable code / phase / recoverable / message), derived from the same failure and published via wcs-camera:error-info-changed / wcs-recorder:error-info-changed. The error shape is unchanged; errorInfo transitions exactly when error does (cleared to null on success). Both elements share one code set (WCS_MEDIA_ERROR_CODE, defined in core/mediaCapabilities.ts):

    • capability-missing (phase probe) — getUserMedia / MediaRecorder unavailable, including non-secure context.
    • not-allowed (phase start) — NotAllowedError / SecurityError (permission denied or feature-policy block).
    • not-found (phase start) — NotFoundError (no camera / mic of the requested kind).
    • not-readable (phase start) — NotReadableError (device busy or hardware fault).
    • invalid-argument (phase start) — OverconstrainedError / NotSupportedError (constraints or mimeType unsatisfiable).
    • invalid-state (phase start) — NoStreamError (recorder started with no stream attached).
    • aborted (phase execute, recoverable: true) — AbortError (interrupted mid-flight, may recover on retry).
    • media-error (phase execute) — any other runtime failure (e.g. RecorderError / unexpected MediaRecorder error).

    The WcsIoErrorInfo type and the WCS_MEDIA_ERROR_CODE constants are exported.

  • Stream ownership stays with the camera. A recorder borrows it; switching cameras while recording is not supported (stop recording first).

  • mimeType support varies (webm/mp4). Unsupported mime-type values are ignored and the browser default is used.

Install

<script type="module" src="https://esm.run/@wcstack/camera/auto"></script>

Or programmatically:

import { bootstrapCamera } from "@wcstack/camera";
bootstrapCamera();

MIT © mogera551