← 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).

Source: Chromium html_media_capture_element_base.idl; Chromium html_media_capture_element_base.h — DEFINE_ATTRIBUTE_EVENT_LISTENER(stream/cancel/error); Specification — event handlers (draft).

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.

Source: Chromium html_media_capture_element_base.cc; Chromium user_media_request_provider_impl.cc.

Outputs

Each event marks the terminal state of one attempt, with the paired property set before dispatch:

EventMeaningState guaranteed at dispatch
streamAcquisition succeededel.stream is a live MediaStream (single track of the element's kind)
cancelUser dismissed or denied the permission promptel.error is NotAllowedError (“Permission dismissed” / “Permission denied”)
errorAny other failureel.error is the failure DOMException (full mapping)
Source: Chromium user_media_request_provider_impl.cc — SetMediaStream/SetError before EnqueueEvent; Chromium html_media_capture_element_base.cc — OnEmbeddedPermissionsDecided.

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.

Source: Chromium html_media_capture_element_base.cc; Specification — activation filtering.

Lifecycle

  1. Per attempt: trusted click → permission flow (may be instant if already granted) → capture request → exactly one terminal event.
  2. Ordering: the paired property (stream or error) is assigned first, the event queued second — handlers can read state immediately.
  3. Re-arm: after cancel/error the element is immediately clickable again; after stream, clicks are ignored until the stream is no longer active (including external track.stop()), at which point a new attempt fires a fresh terminal event.
  4. No repeats: one attempt never fires two terminal events, and a dismissed prompt fires cancel, never error.
Source: Chromium user_media_request_provider_impl.cc; Chromium html_media_capture_element_base.cc.

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 / runtimeSupportNotes
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 / SafariNot supportedNo signal on the ChromeStatus record
BCD / MDNNo entriesNo BCD files; both MDN interface pages 404 (2026-07-28)
Source: ChromeStatus API feature record.

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.

Source: Chromium html_media_capture_element_base.cc — DefaultEventHandler gating; Explainer — security considerations.