← Capability elements: <camera> and <microphone>
v153 · html · event reference
stream / cancel / error events
The acquisition lifecycle signals for <camera> and <microphone>: exactly one terminal event fires per activation attempt — stream on success, cancel when the user declines, error for every other failure. All three are plain Events; the payload lives on the element's stream and error properties.
Syntax
// Shipping Chromium IDL (shared mixin — note "onstream", not the draft's "ontrack"):
interface mixin HTMLMediaCaptureElementBase {
readonly attribute DOMException? error;
attribute EventHandler onstream;
attribute EventHandler oncancel;
attribute EventHandler onerror;
};
// Listener form (equivalent):
el.addEventListener("stream", (event) => { /* el.stream is set */ });
el.addEventListener("cancel", (event) => { /* el.error: NotAllowedError */ });
el.addEventListener("error", (event) => { /* el.error: failure DOMException */ });
All three events are created with Event::Create(...) and queued on the element as DOM-manipulation tasks — they are not cancelable and carry no custom payload. The event-handler content attributes (onstream, oncancel, onerror) work as usual for HTMLElement event handlers. The draft specification names the success event track/ontrack — in the shipping implementation it is stream/onstream (see the overview's spec vs shipping table).
Inputs
Events take no inputs. The handler receives a single plain Event argument whose type is stream, cancel, or error and whose target is the element. Read the element's properties for the outcome.
Outputs
Each event marks the terminal state of one attempt, with the paired property set before dispatch:
| Event | Meaning | State guaranteed at dispatch |
|---|---|---|
stream | Acquisition succeeded | el.stream is a live MediaStream (single track of the element's kind) |
cancel | User dismissed or denied the permission prompt | el.error is NotAllowedError (“Permission dismissed” / “Permission denied”) |
error | Any other failure | el.error is the failure DOMException (full mapping) |
Errors
The events themselves cannot fail, but two outcomes have no event: a click in a non-secure context (fallback content + DevTools audits issue instead) and a click while an acquired stream is still active (silently ignored — no new request, no event). Missing a terminal event for those two cases is the common integration bug; gate your UI on state, not only on events.
Source: Chromium html_media_capture_element_base.cc — DefaultEventHandler; Chromium user_media_request_provider_impl.cc — active-stream early return.Context
Events fire only on element instances created under the CameraAndMicrophoneElements feature, in a Window context, and (for acquisition) in a secure context with a trusted, transient-activation gesture. Because dispatch is task-queued, handlers always run asynchronously after the initiating click — never synchronously inside it.
Lifecycle
- Per attempt: trusted click → permission flow (may be instant if already granted) → capture request → exactly one terminal event.
- Ordering: the paired property (
streamorerror) is assigned first, the event queued second — handlers can read state immediately. - Re-arm: after
cancel/errorthe element is immediately clickable again; afterstream, clicks are ignored until the stream is no longer active (including externaltrack.stop()), at which point a new attempt fires a fresh terminal event. - No repeats: one attempt never fires two terminal events, and a dismissed prompt fires
cancel, nevererror.
Examples
const mic = document.querySelector("microphone");
mic.addEventListener("stream", () => {
// el.stream was assigned before this handler runs:
startRecording(mic.stream);
});
mic.addEventListener("cancel", () => {
// User declined — distinguish dismiss vs deny via the message:
console.log(mic.error.name, mic.error.message); // NotAllowedError "Permission dismissed" | "Permission denied"
showInlineExplainer();
});
mic.addEventListener("error", () => {
switch (mic.error.name) {
case "InvalidStateError": break; // untrusted/no-gesture activation
case "NotFoundError": showNoMicUI(); break;
case "NotReadableError": showDeviceBusyUI(); break;
default: showGenericRetry(mic.error);
}
});
// Attribute form also works:
// <microphone onstream="onMic(this)" oncancel="onCancel(this)" onerror="onErr(this)">
Source: Chromium html_media_capture_element_base.cc; Chromium user_media_request_provider_impl.cc.
Compatibility
| Engine / runtime | Support | Notes |
|---|---|---|
| Chrome / Edge (desktop) | 153 (listing) | Same availability as the elements; trunk flag status caveat in the overview table. stream-named success event is Chromium's shipping contract (draft says track) |
| Chrome (Android) | 153 (listing) | WebView: explicit unknown (see overview) |
| Firefox / Safari | Not supported | No signal on the ChromeStatus record |
| BCD / MDN | No entries | No BCD files; both MDN interface pages 404 (2026-07-28) |
Security and privacy
The events disclose nothing beyond what the triggering gesture already allowed: stream only ever follows the browser's own permission mediation, and cancel/error expose the standard getUserMedia() failure classes. The gesture/trust gating lives upstream of dispatch (untrusted activation is rejected before any of these events can fire), so listeners cannot be triggered programmatically by the page — dispatchEvent(new Event("stream")) would be an untrusted synthetic event that does not run the acquisition path and carries no media.